diff --git a/Cargo.lock b/Cargo.lock index 9ce527f..95f5e92 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -106,6 +106,12 @@ dependencies = [ "alloc-no-stdlib", ] +[[package]] +name = "allocator-api2" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" + [[package]] name = "android-tzdata" version = "0.1.1" @@ -259,6 +265,12 @@ dependencies = [ "rand 0.8.5", ] +[[package]] +name = "arraydeque" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236" + [[package]] name = "arrayref" version = "0.3.8" @@ -379,7 +391,7 @@ checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.85", ] [[package]] @@ -550,7 +562,7 @@ dependencies = [ "proc-macro-crate 3.1.0", "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.85", "syn_derive", ] @@ -649,7 +661,7 @@ checksum = "1ee891b04274a59bd38b412188e24b849617b2e45a0fd8d057deb63e7403761b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.85", ] [[package]] @@ -791,14 +803,13 @@ dependencies = [ [[package]] name = "config" -version = "0.14.0" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7328b20597b53c2454f0b1919720c25c7339051c02b72b7e05409e00b14132be" +checksum = "68578f196d2a33ff61b27fae256c3164f65e36382648e30666dde05b8cc9dfdf" dependencies = [ "async-trait", "convert_case", "json5", - "lazy_static", "nom", "pathdiff", "ron", @@ -806,7 +817,7 @@ dependencies = [ "serde", "serde_json", "toml 0.8.19", - "yaml-rust", + "yaml-rust2", ] [[package]] @@ -893,6 +904,15 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "core2" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b49ba7ef1ad6107f8824dbe97de947cbaac53c44e7f9756a1fba0d37c1eec505" +dependencies = [ + "memchr", +] + [[package]] name = "cpufeatures" version = "0.2.12" @@ -1036,7 +1056,7 @@ dependencies = [ "proc-macro2", "quote", "strsim 0.11.1", - "syn 2.0.72", + "syn 2.0.85", ] [[package]] @@ -1047,7 +1067,7 @@ checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" dependencies = [ "darling_core", "quote", - "syn 2.0.72", + "syn 2.0.85", ] [[package]] @@ -1155,7 +1175,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.85", ] [[package]] @@ -1178,7 +1198,7 @@ checksum = "a6cbae11b3de8fce2a456e8ea3dada226b35fe791f0dc1d360c0941f0bb681f3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.85", ] [[package]] @@ -1275,7 +1295,7 @@ checksum = "a1ab991c1362ac86c61ab6f556cff143daa22e5a15e4e189df818b2fd19fe65b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.85", ] [[package]] @@ -1382,9 +1402,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", "futures-sink", @@ -1392,9 +1412,9 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" [[package]] name = "futures-executor" @@ -1409,38 +1429,38 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" [[package]] name = "futures-macro" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.85", ] [[package]] name = "futures-sink" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" [[package]] name = "futures-task" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" [[package]] name = "futures-util" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ "futures-channel", "futures-core", @@ -1507,6 +1527,20 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" +[[package]] +name = "glass_pumpkin" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c7192ec6aa88ce6bcc2375ddcbf0df89a4f63e4b5b36ffe4ecf8277323cb69" +dependencies = [ + "core2", + "num-bigint 0.4.6", + "num-integer", + "num-traits", + "once_cell", + "rand_core 0.6.4", +] + [[package]] name = "globset" version = "0.4.15" @@ -1542,6 +1576,118 @@ dependencies = [ "scroll", ] +[[package]] +name = "grammers-client" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85c97c2ce2ade23eb00a35d192929a678bd5b1650c7f36eea8a7f0ecd2c79c33" +dependencies = [ + "chrono", + "futures-util", + "grammers-crypto", + "grammers-mtproto", + "grammers-mtsender", + "grammers-session", + "grammers-tl-types", + "locate-locale", + "log", + "md5", + "mime_guess", + "os_info", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "grammers-crypto" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17c75ce8d715d407a5767a94b5fd7b210106ec5c29c7be8ff4dfdca58de4dcf6" +dependencies = [ + "aes", + "getrandom 0.2.15", + "glass_pumpkin", + "hmac 0.12.1", + "num-bigint 0.4.6", + "num-traits", + "pbkdf2 0.12.2", + "sha1", + "sha2 0.10.8", +] + +[[package]] +name = "grammers-mtproto" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d057562ccd5ac7437683534634e469875db27827a7febd42cb6b775ffd01dff" +dependencies = [ + "bytes", + "crc32fast", + "flate2", + "getrandom 0.2.15", + "grammers-crypto", + "grammers-tl-types", + "log", + "num-bigint 0.4.6", + "sha1", +] + +[[package]] +name = "grammers-mtsender" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49b077919dbbacab7644f8479f0c4456625714313334986b4f7c812691aac96a" +dependencies = [ + "bytes", + "futures-util", + "grammers-crypto", + "grammers-mtproto", + "grammers-tl-types", + "log", + "tokio", +] + +[[package]] +name = "grammers-session" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03d5fea440a79a78d0d68b57f062d737a0aed0decee813bf3c232252f3b3f7da" +dependencies = [ + "grammers-crypto", + "grammers-tl-gen", + "grammers-tl-parser", + "grammers-tl-types", + "log", +] + +[[package]] +name = "grammers-tl-gen" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c214cadfba8cd8ef9368bdf273f56f688537b0310ea42f91fc746f4fd59b70b9" +dependencies = [ + "grammers-tl-parser", +] + +[[package]] +name = "grammers-tl-parser" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee695382f42bbeba75c08311785b208d7efa8ded80b33ae387157b07ee21267b" +dependencies = [ + "crc32fast", +] + +[[package]] +name = "grammers-tl-types" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49566409a69e4ff3340c1b6f392e5754c89fa1ea43be4584ac8ee74c1eb0643a" +dependencies = [ + "grammers-tl-gen", + "grammers-tl-parser", +] + [[package]] name = "h2" version = "0.3.26" @@ -1609,6 +1755,19 @@ name = "hashbrown" version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", + "allocator-api2", +] + +[[package]] +name = "hashlink" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7" +dependencies = [ + "hashbrown 0.14.5", +] [[package]] name = "hermit-abi" @@ -2094,18 +2253,21 @@ dependencies = [ "libsecp256k1-core", ] -[[package]] -name = "linked-hash-map" -version = "0.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" - [[package]] name = "linux-raw-sys" version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" +[[package]] +name = "locate-locale" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2835eaaed39a92511442aff277d4dca3d7674ca058df3bc45170661c2ccb4619" +dependencies = [ + "winapi", +] + [[package]] name = "lock_api" version = "0.4.12" @@ -2131,6 +2293,12 @@ dependencies = [ "regex-automata 0.1.10", ] +[[package]] +name = "md5" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" + [[package]] name = "memchr" version = "2.7.4" @@ -2302,6 +2470,7 @@ checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" dependencies = [ "num-integer", "num-traits", + "rand 0.8.5", ] [[package]] @@ -2328,7 +2497,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.85", ] [[package]] @@ -2400,7 +2569,7 @@ dependencies = [ "proc-macro-crate 3.1.0", "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.85", ] [[package]] @@ -2462,7 +2631,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.85", ] [[package]] @@ -2494,12 +2663,22 @@ dependencies = [ [[package]] name = "ordered-multimap" -version = "0.6.0" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ed8acf08e98e744e5384c8bc63ceb0364e68a6854187221c18df61c4797690e" +checksum = "49203cdcae0030493bad186b28da2fa25645fa276a51b6fec8010d281e02ef79" dependencies = [ "dlv-list", - "hashbrown 0.13.2", + "hashbrown 0.14.5", +] + +[[package]] +name = "os_info" +version = "3.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae99c7fa6dd38c7cafe1ec085e804f8f555a2f8659b0dbe03f1f9963a9b51092" +dependencies = [ + "log", + "windows-sys 0.52.0", ] [[package]] @@ -2567,6 +2746,16 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest 0.10.7", + "hmac 0.12.1", +] + [[package]] name = "pem" version = "1.1.1" @@ -2622,7 +2811,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.85", ] [[package]] @@ -2951,9 +3140,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38200e5ee88914975b69f657f0801b6f6dccafd44fd9326302a4aaeecfacb1d8" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", @@ -3160,9 +3349,9 @@ dependencies = [ [[package]] name = "rust-ini" -version = "0.19.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e2a3bcec1f113553ef1c88aae6c020a369d03d55b58de9869a0908930385091" +checksum = "3e0698206bcb8882bf2a9ecb4c1e7785db57ff052297085a6efd4fe42302068a" dependencies = [ "cfg-if", "ordered-multimap", @@ -3347,7 +3536,7 @@ checksum = "1db149f81d46d2deba7cd3c50772474707729550221e69588478ebf9ada425ae" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.85", ] [[package]] @@ -3391,9 +3580,9 @@ checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" [[package]] name = "serde" -version = "1.0.210" +version = "1.0.213" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" +checksum = "3ea7893ff5e2466df8d720bb615088341b295f849602c6956047f8f80f0e9bc1" dependencies = [ "serde_derive", ] @@ -3419,20 +3608,20 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.210" +version = "1.0.213" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" +checksum = "7e85ad2009c50b58e87caa8cd6dac16bdf511bbfb7af6c33df902396aa480fa5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.85", ] [[package]] name = "serde_json" -version = "1.0.128" +version = "1.0.132" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8" +checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03" dependencies = [ "itoa", "memchr", @@ -3448,7 +3637,7 @@ checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.85", ] [[package]] @@ -3491,7 +3680,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.85", ] [[package]] @@ -3614,9 +3803,9 @@ dependencies = [ [[package]] name = "solana-account-decoder" -version = "2.0.13" +version = "2.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d87c6ef8c13eb759fa8d887e12c67afd851799050b6afd501a27726551f52e" +checksum = "ad3f0b04a6f8d8778488fe2c3e77e97866d8b61378c8a4d91e188e1444f98186" dependencies = [ "Inflector", "base64 0.22.1", @@ -3639,9 +3828,9 @@ dependencies = [ [[package]] name = "solana-clap-utils" -version = "2.0.13" +version = "2.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f9709683a4d480a0185827292405cad0b6f414abaa479c7d1dfe5e2194aeec8" +checksum = "855bb216b0862bc10ae515b1400a3f677527881f3e3df51e24bb1ba0bd514fcc" dependencies = [ "chrono", "clap 2.34.0", @@ -3656,9 +3845,9 @@ dependencies = [ [[package]] name = "solana-client" -version = "2.0.13" +version = "2.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67169e4f1faabb717ce81b5ca93960da21e3ac5c9b75cb6792f9b3ce38db459f" +checksum = "e17a2e3cf4aa6b7ed64d33ea656507af4e754832ad3c8733fab6ff5eeb8b4249" dependencies = [ "async-trait", "bincode", @@ -3689,9 +3878,9 @@ dependencies = [ [[package]] name = "solana-compute-budget" -version = "2.0.13" +version = "2.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5acde49a883ca3e099a8050ad8321ea56b02041995dadcf84b0dab14561cc34a" +checksum = "29e90b1be747a3f2373c8b7f2e1bd4291249fc588647524789ff877cc57e7ad8" dependencies = [ "rustc_version", "solana-sdk", @@ -3699,9 +3888,9 @@ dependencies = [ [[package]] name = "solana-config-program" -version = "2.0.13" +version = "2.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f638e44fb308bdc1ce99eb0fee194b2cb212917b258999cdb4a8b056d48973d4" +checksum = "193009c562956c9672cb1fd6439a444367b1b0fd67f20f435ab6a4026e5ed187" dependencies = [ "bincode", "chrono", @@ -3713,9 +3902,9 @@ dependencies = [ [[package]] name = "solana-connection-cache" -version = "2.0.13" +version = "2.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fd01a4d43b780996970cb3669946b002f71d34e6a26a19bd6d2a74513ecc0aa" +checksum = "fe5a15ec1f3f9860e6171e5dd2d497974e7167f0ba15c8215d448b30f3bee12f" dependencies = [ "async-trait", "bincode", @@ -3734,9 +3923,9 @@ dependencies = [ [[package]] name = "solana-curve25519" -version = "2.0.13" +version = "2.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44b61d8eda3319deca3627e3eb3970ce2ad179ad39c106d6c003d06c90e3031d" +checksum = "e0d6ca4dc435e6048f871424cd5ace2aeb06c2c82229d684903b03fb351072d4" dependencies = [ "bytemuck", "bytemuck_derive", @@ -3747,9 +3936,9 @@ dependencies = [ [[package]] name = "solana-inline-spl" -version = "2.0.13" +version = "2.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f6614014b976112fb6c9bf259f87c6659b8fdea628c656639e02211324d2b34" +checksum = "f94111dea93785d063d62894046f2a1c2fd822f107aa59c82272a34d2b98cfd4" dependencies = [ "bytemuck", "rustc_version", @@ -3758,9 +3947,9 @@ dependencies = [ [[package]] name = "solana-logger" -version = "2.0.13" +version = "2.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6b996befdb2bdbd816524fc7afe0e158fced33ff61c36ab29ae803c0462455d" +checksum = "7369915bd82d09dcb14a5ebba3431c4e54f19f2de0521ac56627d38016d45408" dependencies = [ "env_logger", "lazy_static", @@ -3769,9 +3958,9 @@ dependencies = [ [[package]] name = "solana-measure" -version = "2.0.13" +version = "2.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79d44cdbcf9e1489564cdae1cd92b8806b0ee89d05d36a58fef8c0d293ea7c2a" +checksum = "8b583a9a2a43e02231636662663d7804b70c1c0f3a42b1a641ab5d5964bc8ebf" dependencies = [ "log", "solana-sdk", @@ -3779,9 +3968,9 @@ dependencies = [ [[package]] name = "solana-metrics" -version = "2.0.13" +version = "2.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68979964a3a004f1af4f1571814817e7e050ef4c1b2a1bdaa3ff35e980072d69" +checksum = "68602687aeb613bd73933f27ce11b1ad58896a6a2fa71497fecae2987e614f61" dependencies = [ "crossbeam-channel", "gethostname", @@ -3794,9 +3983,9 @@ dependencies = [ [[package]] name = "solana-net-utils" -version = "2.0.13" +version = "2.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44bb419eb9293a277982cf14a58772e9b9ab30ff6f9421bc4ac0826d40122760" +checksum = "44f41b767c25ec128957b73414efe0797877240e1a9f033e910ab3bcd54748be" dependencies = [ "bincode", "clap 3.2.25", @@ -3817,9 +4006,9 @@ dependencies = [ [[package]] name = "solana-perf" -version = "2.0.13" +version = "2.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00c4128122787a61d8f94fdaa04cb71b3dbb017d9939ac4d632264c55ec345de" +checksum = "e4e7809a71f40bc1d551c86203f11c463926dcfe5a892e74357de618aeffe13a" dependencies = [ "ahash", "bincode", @@ -3844,9 +4033,9 @@ dependencies = [ [[package]] name = "solana-program" -version = "2.0.13" +version = "2.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29249ce5b5c7bd018013adbb97439b0b1b986f16bb07c54db28f82e97baaa2f1" +checksum = "2625a23c3813b620141ee447819b08d1b9a5f1c69a309754834e3f35798a21fb" dependencies = [ "ark-bn254", "ark-ec", @@ -3890,9 +4079,9 @@ dependencies = [ [[package]] name = "solana-program-runtime" -version = "2.0.13" +version = "2.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "948bfeb10ba38b55a8b2db2de8ccfa8f57b44b6d73c98e8e0de8b10f10ce043b" +checksum = "9f6f48b286f452feb1f2151ffa4243a382affc126d90c60ad804d2759175b996" dependencies = [ "base64 0.22.1", "bincode", @@ -3919,9 +4108,9 @@ dependencies = [ [[package]] name = "solana-pubsub-client" -version = "2.0.13" +version = "2.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3ce9fa94ef00f7dfec749fc6835a4c36e8cfa2166c4a80736af1b49ef5bcd8e" +checksum = "f03618c313746b6c69ff04c00d210f8c4549b8c9e172ac90eb1d003e810e5353" dependencies = [ "crossbeam-channel", "futures-util", @@ -3944,9 +4133,9 @@ dependencies = [ [[package]] name = "solana-quic-client" -version = "2.0.13" +version = "2.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00764a5e5e36a94515d05f771e869c920671f5753cfc71ebf366546c891450b4" +checksum = "6f54213cdc6f6a869f86603385563a78bc89012cfeb921e0a7ba8710d32f65fe" dependencies = [ "async-mutex", "async-trait", @@ -3970,9 +4159,9 @@ dependencies = [ [[package]] name = "solana-rayon-threadlimit" -version = "2.0.13" +version = "2.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33119350281687a17a8321f897dfd27009fc862711ee6555c26beb5b84d6c08c" +checksum = "369b4d9a8e15906219ee60c1ce6c336ad04fe1217c9c1d625e1960ba5b174d24" dependencies = [ "lazy_static", "num_cpus", @@ -3980,9 +4169,9 @@ dependencies = [ [[package]] name = "solana-remote-wallet" -version = "2.0.13" +version = "2.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aba8725448426110b9ac20d7256f43aad1ea46458fe35c63d174cf962af4a9d0" +checksum = "d9d30c4d54739ca1f56113699c086d2dd5e7423eb5f894d0312757cace61e193" dependencies = [ "console", "dialoguer", @@ -3999,9 +4188,9 @@ dependencies = [ [[package]] name = "solana-rpc-client" -version = "2.0.13" +version = "2.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd96f6a505a492544ee2459b608af3fe07da6c8ffc0bd842489e836ac2c3fce6" +checksum = "a12219fb033d7de4e0fe6c613d7ebea5e457d2ca71890ead6c2a3cb5e4534275" dependencies = [ "async-trait", "base64 0.22.1", @@ -4026,9 +4215,9 @@ dependencies = [ [[package]] name = "solana-rpc-client-api" -version = "2.0.13" +version = "2.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d04f79b88c53b675d5d885d498e7a7e6a4fdd60ffe56e543faddb5d94c6094ba" +checksum = "6dc975656b2bd12d9a0d3b37d7188be83d5d40928d48c50450f4e1adab0eb795" dependencies = [ "anyhow", "base64 0.22.1", @@ -4050,9 +4239,9 @@ dependencies = [ [[package]] name = "solana-rpc-client-nonce-utils" -version = "2.0.13" +version = "2.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42d46d162566cbf7d6eb2ae369fbb8a934bc846906cbe959aed9123c1ac92b85" +checksum = "d251b055f02d2fbbf30da82f10229e2bfe2c4ced7724b8d1311b6184a83589bb" dependencies = [ "clap 2.34.0", "solana-clap-utils", @@ -4063,9 +4252,9 @@ dependencies = [ [[package]] name = "solana-sdk" -version = "2.0.13" +version = "2.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24dae5bda29858add4df3a6c5eaf71c0d2042ca3317a9fd81d7e9f436278a1fe" +checksum = "6bec7d84513d65700740755c512a0d58b9f60dbbce683379c399d2c357b3ceb0" dependencies = [ "bincode", "bitflags 2.6.0", @@ -4112,15 +4301,15 @@ dependencies = [ [[package]] name = "solana-sdk-macro" -version = "2.0.13" +version = "2.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "704c9cacc61a5b9b6f717773cf4b3b45a4239dc7fa8c585258fceaf9b8e1cb94" +checksum = "93a5a1eabc890415d326707afe62cd7a2009236e8d899c1519566fc8f7e3977b" dependencies = [ "bs58", "proc-macro2", "quote", "rustversion", - "syn 2.0.72", + "syn 2.0.85", ] [[package]] @@ -4131,9 +4320,9 @@ checksum = "468aa43b7edb1f9b7b7b686d5c3aeb6630dc1708e86e31343499dd5c4d775183" [[package]] name = "solana-streamer" -version = "2.0.13" +version = "2.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cf77ab19483dce4b4307c9e6f195a8c52f0c219026b78af3a9fae1e63ba9222" +checksum = "9a66633bc2a7bf49f9d94ec7728bc92ca7e1a563ec2d1752b1501170f48392b1" dependencies = [ "async-channel", "bytes", @@ -4165,9 +4354,9 @@ dependencies = [ [[package]] name = "solana-thin-client" -version = "2.0.13" +version = "2.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8c880be4e50ff473b3e82b600162244b6eb28cb5a616dc90ee9232d34998680" +checksum = "dc6717f63e619d8062f9ba9f874dec1fa21e5dbaf85f9a6bc8adba1b97c4df46" dependencies = [ "bincode", "log", @@ -4180,9 +4369,9 @@ dependencies = [ [[package]] name = "solana-tpu-client" -version = "2.0.13" +version = "2.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e65c01edbca303273e735ae383dde54bd5c5b8a051c51162c0ff886b0939ec6" +checksum = "c524c75954fd3ca8f7cdcb386ab70b8861c671a82ed7310e8ed50aa4318b093c" dependencies = [ "async-trait", "bincode", @@ -4204,9 +4393,9 @@ dependencies = [ [[package]] name = "solana-transaction-metrics-tracker" -version = "2.0.13" +version = "2.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44727bef1f8c57a6ed9a74761d8b7ddfcf4b4e2237cbcc5dc7f8f59985e07755" +checksum = "439f96c03d9c2f133b51fa82f227a09cf1f8d5fc63b70d7754d75c02bb7f9e5e" dependencies = [ "Inflector", "base64 0.22.1", @@ -4220,9 +4409,9 @@ dependencies = [ [[package]] name = "solana-transaction-status" -version = "2.0.13" +version = "2.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d51d9d4a6004708f9563a29aa87fdf9960c1e7420b69dd82e8b817cf8f02430b" +checksum = "02b6361f2bb0020a269108e8630c174ad97a72e8ba1fe52a7ccaae27fc1219c7" dependencies = [ "Inflector", "base64 0.22.1", @@ -4247,9 +4436,9 @@ dependencies = [ [[package]] name = "solana-type-overrides" -version = "2.0.13" +version = "2.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ab21276d6296965dc7181d785075b20e97b6789c76e8376cf363b3e2f7439b6" +checksum = "18ee8181704d686981cf4c1365a15f16e1b680542c160ddd4d07ec33e0be747a" dependencies = [ "lazy_static", "rand 0.8.5", @@ -4257,9 +4446,9 @@ dependencies = [ [[package]] name = "solana-udp-client" -version = "2.0.13" +version = "2.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10e902d4dc29cafc0794073805a2db1b48b818251480a9fbaec3959df72aec2f" +checksum = "b50a813338c28da988ec0e0fd8c53f756f93c63f403a3ac46ba2c15138dd60a4" dependencies = [ "async-trait", "solana-connection-cache", @@ -4272,9 +4461,9 @@ dependencies = [ [[package]] name = "solana-version" -version = "2.0.13" +version = "2.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bcbc570264e5a61a8f84439dfc254931460769fedfb91ff16253acfc3644c9d" +checksum = "fe60c22d8bd1325ab3379950dcc14027fa40e3de9a39a0b22645f81803d6cfaf" dependencies = [ "log", "rustc_version", @@ -4286,9 +4475,9 @@ dependencies = [ [[package]] name = "solana-vote" -version = "2.0.13" +version = "2.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fa1401a42023379f14af9165954f44ad02888a327dfd2a4abce0f18fa7cfab9" +checksum = "62a9aa08d6d925b438d569d5e56bad9eb98fd2acd91bf76274ed59045dc77f9f" dependencies = [ "itertools 0.12.1", "log", @@ -4301,9 +4490,9 @@ dependencies = [ [[package]] name = "solana-vote-program" -version = "2.0.13" +version = "2.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfd8e539a9963c2914ff8426dfe92351a902892aea465cd507e36d638ca0b7d6" +checksum = "4e7a9b9023943c6ba747d6e1c5bf16343e510060cbef3f576d8527b33938c48a" dependencies = [ "bincode", "log", @@ -4321,9 +4510,9 @@ dependencies = [ [[package]] name = "solana-zk-token-sdk" -version = "2.0.13" +version = "2.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1dd7a8d6843cb3de4c13c2cfec1994519735ea4110b7f36b80b41d57bea1c07" +checksum = "8c9c1b81daa97afa8690da1a72a453f4c7faf4dc05c4205074b2cbd8f4e5490c" dependencies = [ "aes-gcm-siv", "base64 0.22.1", @@ -4416,7 +4605,7 @@ checksum = "d9e8418ea6269dcfb01c712f0444d2c75542c04448b480e87de59d2865edc750" dependencies = [ "quote", "spl-discriminator-syn", - "syn 2.0.72", + "syn 2.0.85", ] [[package]] @@ -4428,7 +4617,7 @@ dependencies = [ "proc-macro2", "quote", "sha2 0.10.8", - "syn 2.0.72", + "syn 2.0.85", "thiserror", ] @@ -4477,7 +4666,7 @@ dependencies = [ "proc-macro2", "quote", "sha2 0.10.8", - "syn 2.0.72", + "syn 2.0.85", ] [[package]] @@ -4632,9 +4821,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.72" +version = "2.0.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc4b9b9bf2add8093d3f2c0204471e951b2285580335de42f9d2534f3ae7a8af" +checksum = "5023162dfcd14ef8f32034d8bcd4cc5ddc61ef7a247c024a33e24e1f24d21b56" dependencies = [ "proc-macro2", "quote", @@ -4650,7 +4839,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.85", ] [[package]] @@ -4776,22 +4965,22 @@ checksum = "23d434d3f8967a09480fb04132ebe0a3e088c173e6d0ee7897abbdf4eab0f8b9" [[package]] name = "thiserror" -version = "1.0.64" +version = "1.0.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84" +checksum = "5d11abd9594d9b38965ef50805c5e469ca9cc6f197f883f717e0269a3057b3d5" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.64" +version = "1.0.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3" +checksum = "ae71770322cbd277e69d762a16c444af02aa0575ac0d174f0b9562d3b37f8602" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.85", ] [[package]] @@ -4880,7 +5069,7 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "token-scraper-core" -version = "0.1.0" +version = "0.2.0" dependencies = [ "assert_fs", "bincode", @@ -4888,8 +5077,8 @@ dependencies = [ "console", "csv", "futures-util", + "grammers-client", "http 1.1.0", - "indicatif", "raydium-amm-interface", "regex", "reqwest 0.12.8", @@ -4908,9 +5097,9 @@ dependencies = [ [[package]] name = "tokio" -version = "1.40.0" +version = "1.41.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2b070231665d27ad9ec9b8df639893f46727666c6767db40317fbe920a5d998" +checksum = "145f3413504347a2be84393cc8a7d2fb4d863b375909ea59f2158261aa258bbb" dependencies = [ "backtrace", "bytes", @@ -4932,7 +5121,7 @@ checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.85", ] [[package]] @@ -5111,7 +5300,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.85", ] [[package]] @@ -5425,7 +5614,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.85", "wasm-bindgen-shared", ] @@ -5459,7 +5648,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.85", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -5760,12 +5949,14 @@ dependencies = [ ] [[package]] -name = "yaml-rust" -version = "0.4.5" +name = "yaml-rust2" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +checksum = "8902160c4e6f2fb145dbe9d6760a75e3c9522d8bf796ed7047c85919ac7115f8" dependencies = [ - "linked-hash-map", + "arraydeque", + "encoding_rs", + "hashlink", ] [[package]] @@ -5786,7 +5977,7 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.85", ] [[package]] @@ -5806,7 +5997,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.85", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 1eccc85..1a92ddb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,23 +6,23 @@ resolver = "2" assert_fs = "1.1.2" bincode = "1.3.3" borsh = "1.5.1" -config = "0.14.0" +config = "0.14.1" console = "0.15.8" csv = "1.3.0" -futures-util = "0.3.30" +futures-util = "0.3.31" +grammers-client = "0.7.0" http = "1.1.0" -indicatif = "0.17.8" num-derive = "0.4.2" num-traits = "0.2.19" -regex = "1.11.0" +regex = "1.11.1" reqwest = "0.12.8" -serde = "1.0.210" -serde_json = "1.0.128" -solana-client = "2.0.13" -solana-program = "2.0.13" -solana-sdk = "2.0.13" -thiserror = "1.0.64" -tokio = "1.40.0" +serde = "1.0.213" +serde_json = "1.0.132" +solana-client = "2.0.14" +solana-program = "2.0.14" +solana-sdk = "2.0.14" +thiserror = "1.0.65" +tokio = "1.41.0" tokio-tungstenite = "0.24.0" tracing = "0.1.40" tracing-appender = "0.2.3" diff --git a/README.md b/README.md index 4e543a6..3ab1e24 100644 --- a/README.md +++ b/README.md @@ -9,19 +9,20 @@ A minimal CLI application designed to detect and scrape Solana token information ### Supported platforms - [x] Discord -- [ ] Telegram +- [x] Telegram +- [ ] Twitter ## Working and Usage -The program connects to the Discord gateway using your token and monitors messages mentioning valid Solana token addresses. +The program connects to the Discord gateway using your token and the Telegram API using your credentials, monitoring messages mentioning valid Solana token addresses. -- You can use `discord_filters.csv` to customize it and filter scans to specific channels and user messages. +- You can use `filters.csv` to customize it and filter scans to specific channels, groups, and user messages. - This is not a sniping/trading bot but is designed to work alongside automation/sniping bots like [Peppermints](https://www.tensor.trade/trade/peppermints) or your custom programs with similar functionality. - Once a token is detected, it will send a GET request to the URL specified in the `TOKEN_ENDPOINT_URL` field. - The detected token addresses are saved in a local text file `detected_tokens.txt` to avoid duplicate purchases. - Logs are saved in `logs` directory for debugging purposes. -> **IMPORTANT: Using user accounts for automation is against Discord's TOS, so use them at your own risk, preferably with accounts you can afford to lose.** +> **IMPORTANT: Using user accounts for automation is against Discord's TOS, so use them at your own risk, preferably with accounts you can afford to lose. Telegram is more lenient.** ## Installation @@ -39,6 +40,10 @@ Need to have a `settings.json` file in the working directory with the following: "user_token": "YOUR_DISCORD_USER_TOKEN", "sec_ws_key": "YOUR_DISCORD_SECRET_WS_KEY" }, + "telegram": { + "api_id": "YOUR_TELEGRAM_API_ID", + "api_hash": "YOUR_TELEGRAM_API_HASH" + }, "solana": { "rpc_url": "YOUR_RPC_URL" } @@ -47,21 +52,37 @@ Need to have a `settings.json` file in the working directory with the following: - Read how to get a Discord user account token [here](https://gist.github.com/MarvNC/e601f3603df22f36ebd3102c501116c6). - You can similarly obtain the `Sec-Websocket-Key` from the headers of the WebSocket request to the Discord gateway. +- Telegram's `api_id` and `api_hash` can be obtained from [my.telegram.org](https://my.telegram.org). +- You will be asked to enter your phone number to receive a code to authenticate your Telegram account for the first time. Your session will be saved in `scraper.session`, so this won't be needed every time. - `rpc_url` is only used for getting token addresses from links. Won't be used to send transactions. ### Filters -Need to have a `discord_filters.csv` file in the working directory with the following: +Need to have a `filters.csv` file in the working directory with the following: ```csv -NAME,CHANNEL_ID,USER_ID,TOKEN_ENDPOINT_URL -test,12314,123234,http://localhost:9001/solana -pow-calls,132414,51451345,http://localhost:9005/solana +NAME,DISCORD_CHANNEL_ID,DISCORD_USER_ID,TELEGRAM_CHANNEL_ID,TOKEN_ENDPOINT_URL,MARKET_CAP +test,12314,123234,,http://localhost:9001/solana, +pow-calls,132414,51451345,,http://localhost:9005/solana,20000 ``` -- `CHANNEL_ID` is the ID of the channel you want to monitor. -- `USER_ID` is the ID of the user you want to monitor. -- `TOKEN_ENDPOINT_URL` is the URL to which a GET request will be made, with the token address as a parameter. +- `NAME`(Required): The name of the filter. This is just for your reference. +- `DISCORD_CHANNEL_ID`(Optional): The ID of the Discord channel you want to monitor. +- `DISCORD_USER_ID`(Optional): The ID of the Discord user you wish to monitor. +- `TELEGRAM_CHANNEL_ID`(Optional):The ID of the Telegram channel you wish to monitor. For private Telegram channels, which start with `-100`, ensure that the `-100` is removed from the ID. If you’re unsure how to find a Telegram channel ID, a quick online search can guide you. +- `TOKEN_ENDPOINT_URL`(Required): The URL to which a GET request will be made, with the token address as a parameter. +- `MARKET_CAP`(Optional): Represents the minimum market capitalization required for the token to be detected. The token’s price is retrieved via Jupiter’s API; however, this may not be applicable for very new tokens. + +Discord fields are combined using an AND operation, whereas Discord and Telegram fields are combined using an OR operation. For example, in the filter below: + +```csv +NAME,DISCORD_CHANNEL_ID,DISCORD_USER_ID,TELEGRAM_CHANNEL_ID,TOKEN_ENDPOINT_URL,MARKET_CAP +test,12314,123234,2254310975,http://localhost:9001/solana, +``` + +The token is detected if it is posted in the `DISCORD_CHANNEL_ID` of `12314` by the `DISCORD_USER_ID` `123234`, OR if it is posted by anyone in the `TELEGRAM_CHANNEL_ID` `2254310975`. + +**Note:** Currently, filtering by USER ID is not supported for Telegram. ## Support and Contact diff --git a/token-scraper-core/Cargo.toml b/token-scraper-core/Cargo.toml index 32daaca..794adfa 100644 --- a/token-scraper-core/Cargo.toml +++ b/token-scraper-core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "token-scraper-core" -version = "0.1.0" +version = "0.2.0" authors = ["Lezend"] description = "CLI tool to scrape solana tokens." license = "MIT OR Apache-2.0" @@ -12,8 +12,8 @@ config = { workspace = true } console = { workspace = true } csv = { workspace = true } futures-util = { workspace = true, features = ["sink"] } +grammers-client = { workspace = true } http = { workspace = true } -indicatif = { workspace = true } raydium-amm-interface = { workspace = true, features = ["serde"] } regex = { workspace = true } reqwest = { workspace = true, features = ["json"] } diff --git a/token-scraper-core/src/discord/stream/handler.rs b/token-scraper-core/src/discord/stream/handler.rs index 143b355..9061045 100644 --- a/token-scraper-core/src/discord/stream/handler.rs +++ b/token-scraper-core/src/discord/stream/handler.rs @@ -76,9 +76,9 @@ pub async fn handle_stream( let task = tokio::spawn(async move { match event.unwrap() { GatewayEvent::Hello(data) => { - tracing::info!("Received hello event: {:?}", data); + tracing::trace!("Received hello event: {:?}", data); - tracing::info!("Sending heartbeat"); + tracing::trace!("Sending heartbeat"); send_heartbeat(Arc::clone(&sequence), Arc::clone(&ws_write)) .await .expect("Failed to send heartbeat"); @@ -94,7 +94,7 @@ pub async fn handle_stream( if let Err(e) = send_heartbeat(Arc::clone(&sequence), Arc::clone(&ws_write)).await { - tracing::error!("Failed to send heartbeat: {:?}", e); + tracing::debug!("Failed to send heartbeat: {:?}", e); break; } } @@ -169,8 +169,8 @@ async fn handle_exception( match event_type { "READY" => { - tracing::info!("Received ready event"); - tracing::info!( + tracing::debug!("Received ready event"); + tracing::debug!( "Logged in as {}", json_value .get("d") diff --git a/token-scraper-core/src/discord/stream/identify.rs b/token-scraper-core/src/discord/stream/identify.rs index 3fd2a69..74ef726 100644 --- a/token-scraper-core/src/discord/stream/identify.rs +++ b/token-scraper-core/src/discord/stream/identify.rs @@ -33,7 +33,7 @@ pub async fn identify(discord_token: String, ws_write: WebsocketWrite) -> Result )) .await?; - tracing::info!("Identified with Discord"); + tracing::debug!("Identified with Discord"); Ok(()) } diff --git a/token-scraper-core/src/discord/stream/mod.rs b/token-scraper-core/src/discord/stream/mod.rs index a6f37ff..4a1916f 100644 --- a/token-scraper-core/src/discord/stream/mod.rs +++ b/token-scraper-core/src/discord/stream/mod.rs @@ -10,14 +10,10 @@ mod ws_request; use std::sync::Arc; use futures_util::StreamExt; -use indicatif::{ProgressBar, ProgressStyle}; -use std::time::Duration; use tokio::sync::{mpsc::UnboundedSender, Mutex}; use tokio_tungstenite::connect_async; use twilight_model::gateway::event::DispatchEvent; -use crate::get_spinner; - use self::handler::handle_stream; /// Error types for the Discord stream module. @@ -54,22 +50,16 @@ pub async fn start_stream( sec_ws_key: &str, event_tx: Arc>, ) -> Result<(), Error> { - let spinner = get_spinner!("Connecting to discord..."); - let request = ws_request::create_request_with_headers(sec_ws_key.to_string()).await?; let (ws_stream, _) = connect_async(request).await?; let (ws_write, ws_read) = ws_stream.split(); - spinner.finish(); - let ws_write = Arc::new(Mutex::new(ws_write)); let sequence: Arc>> = Arc::new(Mutex::new(None)); let resume_gateway_url: Arc>> = Arc::new(Mutex::new(None)); let session_id: Arc>> = Arc::new(Mutex::new(None)); - println!("Watching for messages..."); - handle_stream( discord_token, ws_read, diff --git a/token-scraper-core/src/discord/stream/util.rs b/token-scraper-core/src/discord/stream/util.rs index e756862..c0ee157 100644 --- a/token-scraper-core/src/discord/stream/util.rs +++ b/token-scraper-core/src/discord/stream/util.rs @@ -89,7 +89,7 @@ pub async fn send_heartbeat( .await .map_err(HeartbeatError::Ws)?; - tracing::debug!("Sent heartbeat: {:?}", payload); + tracing::trace!("Sent heartbeat: {:?}", payload); Ok(()) } diff --git a/token-scraper-core/src/filters.rs b/token-scraper-core/src/filters.rs new file mode 100644 index 0000000..7ca3a52 --- /dev/null +++ b/token-scraper-core/src/filters.rs @@ -0,0 +1,68 @@ +//! Filters module for the token-scraper application. + +use std::path::Path; + +use serde::Deserialize; + +/// Represents a filter for the token-scraper application. +/// +/// This struct is used to define various filters that can be applied to the token-scraper application. +/// It includes fields for filtering by Discord channel ID, Discord user ID, Telegram channel ID, +/// token endpoint URL, and market cap. +#[derive(Debug, Deserialize, Clone)] +pub struct Filter { + /// The name of the filter. + #[serde(rename = "NAME")] + pub name: String, + /// The Discord channel ID to filter. + #[serde(rename = "DISCORD_CHANNEL_ID")] + pub discord_channel_id: Option, + /// The Discord user ID to filter. + #[serde(rename = "DISCORD_USER_ID")] + pub discord_user_id: Option, + /// The Telegram channel ID to filter. + #[serde(rename = "TELEGRAM_CHANNEL_ID")] + pub telegram_channel_id: Option, + /// The token endpoint URL to send to. + #[serde(rename = "TOKEN_ENDPOINT_URL")] + pub token_endpoint_url: String, + /// The market cap to filter. + #[serde(rename = "MARKET_CAP")] + pub market_cap: Option, +} + +#[derive(thiserror::Error, Debug)] +pub enum FiltersError { + /// File error. + #[error("File error: {0}")] + File(#[from] std::io::Error), + + /// Deserialization error. + #[error("Deserialization error: {0}")] + Deserialize(#[from] csv::Error), +} + +/// Reads filters from a CSV file. +/// +/// This function opens the specified CSV file, reads its contents, and deserializes each record into a `Filter` struct. +/// +/// # Arguments +/// +/// * `file_path` - A `&Path` that holds the path to the CSV file. +/// +/// # Errors +/// +/// This function will return an error if the file cannot be opened or if deserialization fails. +pub fn read_filters_from_csv(file_path: &Path) -> Result, FiltersError> { + let mut filters = Vec::new(); + let file = std::fs::File::open(file_path)?; + let reader = std::io::BufReader::new(file); + let mut rdr = csv::Reader::from_reader(reader); + + for result in rdr.deserialize() { + let record: Filter = result?; + filters.push(record); + } + + Ok(filters) +} diff --git a/token-scraper-core/src/main.rs b/token-scraper-core/src/main.rs index 2d9d571..190afae 100644 --- a/token-scraper-core/src/main.rs +++ b/token-scraper-core/src/main.rs @@ -10,17 +10,20 @@ )] mod discord; +mod filters; mod macros; mod message_handler; mod photon_util; mod settings; +mod telegram; mod util; use std::{path::Path, sync::Arc}; use discord::stream::start_stream; +use filters::read_filters_from_csv; use message_handler::handle_message; -use settings::read_discord_filters_from_csv; +use telegram::{authorize_client, connect_to_telegram, SESSION_FILE}; use tokio::sync::{mpsc, Mutex}; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter, Layer}; use twilight_model::gateway::event::DispatchEvent; @@ -30,60 +33,109 @@ use crate::settings::Settings; /// Path to the detected tokens file. pub const DETECTED_TOKENS_FILE_PATH: &str = "detected_tokens.txt"; -/// Path to the Discord filters file. -pub const DISCORD_FILTERS_FILE_PATH: &str = "discord_filters.csv"; +/// Path to the filters file. +pub const FILTERS_FILE_PATH: &str = "filters.csv"; #[tokio::main] async fn main() -> Result<(), Box> { let settings = Settings::new()?; - let discord_filters = read_discord_filters_from_csv(Path::new(DISCORD_FILTERS_FILE_PATH))?; + let filters = read_filters_from_csv(Path::new(FILTERS_FILE_PATH))?; // Create the detected tokens file if it doesn't exist if !Path::new(DETECTED_TOKENS_FILE_PATH).exists() { std::fs::File::create(DETECTED_TOKENS_FILE_PATH)?; } // Setup logging - // Create a file layer with info level filtering - let file_appender = tracing_appender::rolling::daily("logs", "token-scraper.log"); + // Create a file layer with debug level filtering + let file_appender = tracing_appender::rolling::daily("logs", ".log"); let (file_writer, _file_writer_guard) = tracing_appender::non_blocking(file_appender); let file_layer = tracing_subscriber::fmt::layer() .json() .with_writer(file_writer) - .with_filter(EnvFilter::new("info")); - // .with_filter(EnvFilter::new("debug")); - - tracing_subscriber::registry().with(file_layer).init(); - - let (discord_event_tx, mut discord_event_rx) = mpsc::unbounded_channel(); - - // Spawn a task to manage the Discord event stream - tokio::spawn(manage_discord_stream( - settings.discord.user_token.clone(), - settings.discord.sec_ws_key.clone(), - discord_event_tx, - )); + .with_filter(EnvFilter::new("token_scraper=debug")); + + // Create a console layer with info level filtering + let console_layer = tracing_subscriber::fmt::layer() + .with_target(false) + .with_filter(EnvFilter::new("token_scraper=info")); + + tracing_subscriber::registry() + .with(file_layer) + .with(console_layer) + .init(); + + // Start the Telegram module + if let Some(telegram_settings) = settings.telegram { + let telegram_span = tracing::span!(tracing::Level::DEBUG, "telegram_module"); + let _telegram_enter = telegram_span.enter(); + + let telegram_client = connect_to_telegram( + telegram_settings.api_id, + telegram_settings.api_hash, + SESSION_FILE, + ) + .await?; + let mut sign_out = false; + authorize_client(&telegram_client, SESSION_FILE, &mut sign_out).await?; - // Main event loop - while let Some(event) = discord_event_rx.recv().await { - let discord_filters = discord_filters.clone(); + let filters = filters.clone(); let rpc_url = settings.solana.rpc_url.clone(); - tokio::spawn(async move { - if let DispatchEvent::MessageCreate(message) = event { - if let Err(e) = handle_message( - &message, - Path::new(DETECTED_TOKENS_FILE_PATH), - &discord_filters, - &rpc_url, - ) - .await - { - tracing::error!("Error while handling discord message: {:?}", e); - } + let result = telegram::start( + &telegram_client, + &filters, + &rpc_url, + Path::new(DETECTED_TOKENS_FILE_PATH), + ) + .await; + + if let Err(e) = result { + tracing::error!("Error while handling telegram message: {}", e); + tracing::debug!("Error: {:?}", e); } }); } + // Start the Discord module + if let Some(discord_settings) = settings.discord { + let discord_span = tracing::span!(tracing::Level::DEBUG, "discord_module"); + let _discord_enter = discord_span.enter(); + + tracing::info!("Starting Discord module.."); + let (discord_event_tx, mut discord_event_rx) = mpsc::unbounded_channel(); + + // Spawn a task to manage the Discord event stream + tokio::spawn(manage_discord_stream( + discord_settings.user_token.clone(), + discord_settings.sec_ws_key.clone(), + discord_event_tx, + )); + + // Main event loop + while let Some(event) = discord_event_rx.recv().await { + let filters = filters.clone(); + let rpc_url = settings.solana.rpc_url.clone(); + + tokio::spawn(async move { + if let DispatchEvent::MessageCreate(message) = event { + if let Err(e) = handle_message( + &message, + Path::new(DETECTED_TOKENS_FILE_PATH), + &filters, + &rpc_url, + ) + .await + { + tracing::error!("Error while handling discord message: {}", e); + tracing::debug!("Error: {:?}", e); + } + } + }); + } + } + + tracing::error!("Neither Telegram nor Discord settings found, exiting..."); + Ok(()) } @@ -95,7 +147,7 @@ async fn manage_discord_stream( ) { let attempts = Arc::new(Mutex::new(0)); loop { - tracing::info!("Attempting to start stream"); + tracing::debug!("Attempting to start discord stream"); // Reset attempts every 60 seconds let attempts_clone = Arc::clone(&attempts); @@ -107,7 +159,6 @@ async fn manage_discord_stream( let result = start_stream(&discord_token, &sec_ws_key, Arc::new(event_tx.clone())).await; if let Err(e) = result { tracing::debug!("Stream error: {e:?}"); - println!("Connection closed, retrying..."); let mut attempts = attempts.lock().await; *attempts += 1; if *attempts >= 3 { @@ -117,11 +168,4 @@ async fn manage_discord_stream( tokio::time::sleep(std::time::Duration::from_secs(5)).await; } tracing::error!("Stream errored after 3 attempts"); - println!( - "{}", - console::style( - "Discord stream errored after multiple retries. Please check logs for more details and try restarting the program." - ) - .red() - ); } diff --git a/token-scraper-core/src/message_handler.rs b/token-scraper-core/src/message_handler.rs index 24d0513..81f35a6 100644 --- a/token-scraper-core/src/message_handler.rs +++ b/token-scraper-core/src/message_handler.rs @@ -6,9 +6,9 @@ use solana_sdk::pubkey::Pubkey; use twilight_model::gateway::payload::incoming::MessageCreate; use crate::{ + filters::Filter, photon_util::{self, is_photon_link}, - settings::DiscordFilter, - util::{self, *}, + util::*, }; /// Errors that can occur when handling a message. @@ -48,14 +48,14 @@ pub enum Error { pub async fn handle_message( message: &MessageCreate, detected_tokens_file_path: &Path, - discord_filters: &[DiscordFilter], + filters: &[Filter], rpc_url: &str, ) -> Result<(), Error> { if message.guild_id.is_none() { return Ok(()); } - let filter = match filter_message(message, discord_filters) { + let filter = match filter_message(message, filters) { Some(f) => f, None => return Ok(()), }; @@ -65,65 +65,46 @@ pub async fn handle_message( None => return Ok(()), }; - if let Some(max_market_cap) = filter.market_cap { - let market_cap_res = get_market_cap(&token.to_string()).await; - match market_cap_res { - Ok(market_cap) => { - if market_cap > max_market_cap { - return Ok(()); - } else { - tracing::debug!("Market cap is too high: {}", market_cap); - } - } - Err(MarketCapError::GetTokenPriceJup(JupPriceApiError::PriceNotFound(resp))) => { - tracing::debug!("Price not found: {}", resp); - tracing::debug!("Proceeding with sniper request") - } - Err(err) => { - return Err(Error::MarketCap(err)); - } + if let Some(mc_threshold) = filter.market_cap { + if !filter_market_cap(&token.to_string(), mc_threshold).await? { + return Ok(()); } } - tracing::info!("Found {} for filter: {}", token.to_string(), filter.name); - if is_token_already_detected(&token.to_string(), detected_tokens_file_path).await? { - tracing::info!("Token already detected, skipping"); + tracing::info!("Token {} already detected, skipping", token); return Ok(()); } - println!( - "Token {} detected for filter: {}", - console::style(token.to_string()).green(), - console::style(filter.name.clone()).yellow() + tracing::info!( + "Token detected for {}: {}", + filter.name, + console::style(token).green() ); send_token_request(&token.to_string(), &filter.token_endpoint_url).await?; add_token_to_file(&token.to_string(), detected_tokens_file_path).await?; - tracing::info!("Successfully sent token to endpoint: {}", token.to_string()); + tracing::debug!("Successfully sent request to endpoint for token: {}", token); Ok(()) } -/// Filters a message based on the provided Discord filters. +/// Filters a message based on the provided filters. /// -/// This function iterates through the provided `discord_filters` and checks if the message matches any of the filters. -/// If a match is found, the corresponding `DiscordFilter` is returned. +/// This function iterates through the provided `filters` and checks if the message matches any of the filters. +/// If a match is found, the corresponding `Filter` is returned. /// /// # Arguments /// /// * `message` - A reference to the `MessageCreate` object. -/// * `discord_filters` - A slice of `DiscordFilter` objects to be checked against. +/// * `filters` - A slice of `Filter` objects to be checked against. /// /// # Returns /// -/// * `Some(DiscordFilter)` if a matching filter is found, otherwise `None`. -fn filter_message( - message: &MessageCreate, - discord_filters: &[DiscordFilter], -) -> Option { - for filter in discord_filters { - match (filter.channel_id, filter.user_id) { +/// * `Some(Filter)` if a matching filter is found, otherwise `None`. +fn filter_message(message: &MessageCreate, filters: &[Filter]) -> Option { + for filter in filters { + match (filter.discord_channel_id, filter.discord_user_id) { (Some(channel_id), Some(user_id)) => { if message.channel_id.get() == channel_id && message.author.id.get() == user_id { return Some(filter.clone()); @@ -162,8 +143,6 @@ async fn process_message_for_token( message: &MessageCreate, rpc_url: &str, ) -> Result, ExtractTokenError> { - tracing::debug!("Processing message: {:?}", message); - // Attempt to extract a token from the message content if let Some(token) = extract_token(&message.content, rpc_url).await? { return Ok(Some(token)); @@ -207,7 +186,10 @@ pub enum ExtractTokenError { /// # Errors /// /// Returns an `ExtractTokenError` if the token extraction fails. -async fn extract_token(content: &str, rpc_url: &str) -> Result, ExtractTokenError> { +pub async fn extract_token( + content: &str, + rpc_url: &str, +) -> Result, ExtractTokenError> { for word in content.split_whitespace() { if is_valid_token_address(word) { return Ok(Some(Pubkey::from_str(word).unwrap())); @@ -222,32 +204,3 @@ async fn extract_token(content: &str, rpc_url: &str) -> Result, E Ok(None) } - -/// Errors that can occur when getting the market cap. -#[derive(Debug, thiserror::Error)] -pub enum MarketCapError { - /// Error from the `util::get_token_price_jup` function. - #[error("Failed to get token price from Jupiter API: {0}")] - GetTokenPriceJup(#[from] util::JupPriceApiError), -} - -/// Fetches the market cap of a token. -/// -/// This function calculates the market cap of a token by fetching its price from the Jupiter API -/// and multiplying it by the supply of pumpfun tokens. -/// -/// # Arguments -/// -/// * `token` - A string slice that holds the token symbol. -/// -/// # Errors -/// -/// Returns a `MarketCapError` if the request to fetch the token price fails. -async fn get_market_cap(token: &str) -> Result { - /// The supply of pumpfun tokens. - const PUMPFUN_TOKEN_SUPPLY: u128 = 1_000_000_000; - - let price = util::get_token_price_jup(token, "USDC").await?; - let market_cap = (price * PUMPFUN_TOKEN_SUPPLY as f64) as u128; - Ok(market_cap) -} diff --git a/token-scraper-core/src/settings.rs b/token-scraper-core/src/settings.rs index ce5b386..ae3cade 100644 --- a/token-scraper-core/src/settings.rs +++ b/token-scraper-core/src/settings.rs @@ -3,8 +3,6 @@ //! This module handles loading and managing the configuration settings for the application. //! It provides a `Settings` struct that holds the configuration values and a method to load these values from a JSON file. -use std::path::Path; - use config::{Config, ConfigError, File, FileFormat}; use serde::Deserialize; use thiserror::Error; @@ -30,9 +28,11 @@ pub enum Error { #[derive(Debug, Deserialize)] pub struct Settings { /// Discord settings. - pub discord: DiscordSettings, + pub discord: Option, /// Solana settings. pub solana: SolanaSettings, + /// Telegram settings. + pub telegram: Option, } /// Discord settings for the token-scraper application. @@ -44,6 +44,15 @@ pub struct DiscordSettings { pub sec_ws_key: String, } +/// Telegram settings for the token-scraper application. +#[derive(Debug, Deserialize)] +pub struct TelegramSettings { + /// API ID for the Telegram client. + pub api_id: i32, + /// API hash for the Telegram client. + pub api_hash: String, +} + /// Solana settings for the token-scraper application. #[derive(Debug, Deserialize)] pub struct SolanaSettings { @@ -68,61 +77,3 @@ impl Settings { Ok(settings) } } - -/// Discord filter for the token-scraper application. -#[derive(Debug, Deserialize, Clone)] -pub struct DiscordFilter { - /// Name of the filter. - #[serde(rename = "NAME")] - pub name: String, - /// Channel ID to filter. - #[serde(rename = "CHANNEL_ID")] - pub channel_id: Option, - /// User ID to filter. - #[serde(rename = "USER_ID")] - pub user_id: Option, - /// Token endpoint URL to filter. - #[serde(rename = "TOKEN_ENDPOINT_URL")] - pub token_endpoint_url: String, - /// Market cap to filter. - #[serde(rename = "MARKET_CAP")] - pub market_cap: Option, -} - -#[derive(Error, Debug)] -pub enum DiscordFiltersError { - /// File error. - #[error("File error: {0}")] - File(#[from] std::io::Error), - - /// Deserialization error. - #[error("Deserialization error: {0}")] - Deserialize(#[from] csv::Error), -} - -/// Reads Discord filters from a CSV file. -/// -/// This function opens the specified CSV file, reads its contents, and deserializes each record into a `DiscordFilter` struct. -/// -/// # Arguments -/// -/// * `file_path` - A `&Path` that holds the path to the CSV file. -/// -/// # Errors -/// -/// This function will return an error if the file cannot be opened or if deserialization fails. -pub fn read_discord_filters_from_csv( - file_path: &Path, -) -> Result, DiscordFiltersError> { - let mut filters = Vec::new(); - let file = std::fs::File::open(file_path)?; - let reader = std::io::BufReader::new(file); - let mut rdr = csv::Reader::from_reader(reader); - - for result in rdr.deserialize() { - let record: DiscordFilter = result?; - filters.push(record); - } - - Ok(filters) -} diff --git a/token-scraper-core/src/telegram/constants.rs b/token-scraper-core/src/telegram/constants.rs new file mode 100644 index 0000000..8cde665 --- /dev/null +++ b/token-scraper-core/src/telegram/constants.rs @@ -0,0 +1,4 @@ +//! Telegram constants. + +/// Path to the session file. +pub const SESSION_FILE: &str = "scraper.session"; diff --git a/token-scraper-core/src/telegram/errors.rs b/token-scraper-core/src/telegram/errors.rs new file mode 100644 index 0000000..d2a7863 --- /dev/null +++ b/token-scraper-core/src/telegram/errors.rs @@ -0,0 +1,69 @@ +//! Telegram errors. + +use crate::{message_handler::ExtractTokenError, util::MarketCapError}; + +/// Errors that can occur in the Telegram module. +/// +/// This enum represents the possible errors that can occur while interacting with Telegram. +#[derive(Debug, thiserror::Error)] +pub enum TelegramError { + /// Error when handling an update. + #[error("Failed to handle update: {0}")] + UpdateHandling(#[from] grammers_client::InvocationError), + + /// Error when extracting a token. + #[error("Failed to extract token: {0}")] + TokenExtraction(#[from] ExtractTokenError), + + /// Error when adding a token to file. + #[error("Failed to add token to file: {0}")] + AddTokenToFile(#[from] std::io::Error), + + /// Error when sending a token request. + #[error("Failed to send token request: {0}")] + SendTokenRequest(#[from] reqwest::Error), + + /// Error when filtering market cap. + #[error("Failed to filter market cap: {0}")] + FilterMarketCap(#[from] MarketCapError), +} + +/// Error types for Telegram connection. +#[derive(Debug, thiserror::Error)] +pub enum TelegramConnectionError { + /// Error when loading or creating the session file. + #[error("Failed to load or create session file: {0}")] + SessionFile(#[from] std::io::Error), + + /// Error when authorizing the client. + #[error("Failed to authorize: {0}")] + Authorization(#[from] grammers_client::client::auth::AuthorizationError), +} + +/// Error types for prompt input. +#[derive(Debug, thiserror::Error)] +pub enum PromptError { + /// Error when reading a line from standard input. + #[error("Failed to read line: {0}")] + ReadLine(#[from] std::io::Error), +} + +/// Error types for authorization. +#[derive(Debug, thiserror::Error)] +pub enum AuthorizationError { + /// Error when prompting the user for input. + #[error("Failed to prompt user for input: {0}")] + Prompt(#[from] PromptError), + + /// Error when invoking a Grammers client method. + #[error("Failed to invoke Grammers client method: {0}")] + GrammersInvocation(#[from] grammers_client::InvocationError), + + /// Error when authorizing the client with phone number. + #[error("Failed to authorize with phone number: {0}")] + PhoneAuthorization(#[from] grammers_client::client::auth::AuthorizationError), + + /// Error when authorizing the client with a password. + #[error("Failed to authorize with password: {0}")] + PasswordAuthorization(#[from] Box), +} diff --git a/token-scraper-core/src/telegram/handler.rs b/token-scraper-core/src/telegram/handler.rs new file mode 100644 index 0000000..8ea26e1 --- /dev/null +++ b/token-scraper-core/src/telegram/handler.rs @@ -0,0 +1,118 @@ +//! Telegram handler module. + +use std::path::Path; + +use grammers_client::Update; + +use crate::{ + filters::Filter, + message_handler::extract_token, + util::{add_token_to_file, filter_market_cap, is_token_already_detected, send_token_request}, +}; + +use super::{errors::*, util::*}; + +/// Starts the Telegram client and processes incoming messages. +/// +/// This function connects to the Telegram client, authorizes it, and continuously listens for new messages. +/// If a message matches any of the provided filters, it extracts the token and sends a request to the specified endpoint. +/// +/// # Arguments +/// +/// * `api_id` - The API ID for the Telegram client. +/// * `api_hash` - The API hash for the Telegram client. +/// * `filters` - A slice of `Filter` objects to be checked against. +/// * `solana_rpc_url` - The RPC URL for Solana. +/// * `detected_tokens_file_path` - A reference to the path of the file where detected tokens are stored. +/// +/// # Errors +/// +/// Returns a `TelegramError` if any step in the process fails. +pub async fn start( + client: &grammers_client::Client, + filters: &[Filter], + solana_rpc_url: &str, + detected_tokens_file_path: &Path, +) -> Result<(), TelegramError> { + tracing::info!("Connected to Telegram!"); + + loop { + let update = client.next_update().await?; + match update { + Update::NewMessage(message) if !message.outgoing() => { + let filters = filters.to_vec(); + let solana_rpc_url = solana_rpc_url.to_string(); + let detected_tokens_file_path = detected_tokens_file_path.to_path_buf(); + + tokio::spawn(async move { + let result = process_message( + message, + &filters, + &solana_rpc_url, + &detected_tokens_file_path, + ) + .await; + + if let Err(e) = result { + tracing::error!("Failed to process message: {:?}", e); + } + }); + } + _ => {} + } + } +} + +/// Processes a Telegram message to detect and handle tokens. +/// +/// This function checks if the message matches any of the provided filters. If a match is found, +/// it extracts the token from the message, sends a request to the token endpoint, and adds the token +/// to the specified file. +/// +/// # Arguments +/// +/// * `message` - The Telegram message to be processed. +/// * `filters` - A slice of `Filter` objects to be checked against. +/// * `solana_rpc_url` - The RPC URL for Solana. +/// * `detected_tokens_file_path` - A reference to the path of the file where detected tokens are stored. +/// +/// # Errors +/// +/// Returns a `TelegramError` if any step in the process fails. +async fn process_message( + message: grammers_client::types::Message, + filters: &[Filter], + solana_rpc_url: &str, + detected_tokens_file_path: &Path, +) -> Result<(), TelegramError> { + let channel_id = message.chat().id(); + if let Some(filter) = filter_message(channel_id, filters) { + let token = extract_token(message.text(), solana_rpc_url).await?; + if let Some(token) = token { + tracing::info!( + "Token detected for {}: {}", + filter.name, + console::style(token).green() + ); + + if let Some(market_cap) = filter.market_cap { + if !filter_market_cap(&token.to_string(), market_cap).await? { + return Ok(()); + } + } + + if is_token_already_detected(&token.to_string(), detected_tokens_file_path).await? { + tracing::info!("Token {} already detected, skipping", token); + return Ok(()); + } + + send_token_request(&token.to_string(), &filter.token_endpoint_url).await?; + add_token_to_file(&token.to_string(), detected_tokens_file_path).await?; + tracing::debug!("Successfully sent request to endpoint for token: {}", token); + } else { + tracing::debug!("No token detected in message for {}", filter.name); + } + } + + Ok(()) +} diff --git a/token-scraper-core/src/telegram/mod.rs b/token-scraper-core/src/telegram/mod.rs new file mode 100644 index 0000000..c6ba601 --- /dev/null +++ b/token-scraper-core/src/telegram/mod.rs @@ -0,0 +1,11 @@ +//! Telegram module for the token-scraper application. + +mod constants; +mod errors; +mod handler; +mod retry; +mod util; + +pub use constants::*; +pub use handler::start; +pub use util::{authorize_client, connect_to_telegram}; diff --git a/token-scraper-core/src/telegram/retry.rs b/token-scraper-core/src/telegram/retry.rs new file mode 100644 index 0000000..f69f91e --- /dev/null +++ b/token-scraper-core/src/telegram/retry.rs @@ -0,0 +1,22 @@ +//! Retry policy for Telegram reconnections. + +use grammers_client::ReconnectionPolicy; + +/// Retry policy for Telegram reconnections. +pub struct RetryPolicy; + +impl ReconnectionPolicy for RetryPolicy { + /// Determines whether to retry the connection based on the number of attempts. + /// + /// # Arguments + /// + /// * `attempts` - The current number of reconnection attempts. + /// + /// # Errors + /// + /// Logs an error if the maximum number of reconnection attempts is reached. + fn should_retry(&self, attempts: usize) -> std::ops::ControlFlow<(), std::time::Duration> { + tracing::debug!("Reconnecting to Telegram... (Attempt {})", attempts); + std::ops::ControlFlow::Continue(std::time::Duration::from_secs(1)) + } +} diff --git a/token-scraper-core/src/telegram/util.rs b/token-scraper-core/src/telegram/util.rs new file mode 100644 index 0000000..825a625 --- /dev/null +++ b/token-scraper-core/src/telegram/util.rs @@ -0,0 +1,141 @@ +//! Telegram utility functions. + +use std::io::{BufRead, Write}; + +use grammers_client::{session::Session, InitParams, SignInError}; + +use crate::filters::Filter; + +use super::{errors::*, retry::RetryPolicy}; + +/// Connects to the Telegram client. +/// +/// This function establishes a connection to the Telegram client using the provided +/// API ID, API hash, and session file. +/// +/// # Arguments +/// +/// * `api_id` - The API ID for the Telegram client. +/// * `api_hash` - The API hash for the Telegram client. +/// * `session_file` - The path to the session file. +/// ``` +pub async fn connect_to_telegram( + api_id: i32, + api_hash: String, + session_file: &str, +) -> Result { + Ok(grammers_client::Client::connect(grammers_client::Config { + session: Session::load_file_or_create(session_file)?, + api_id, + api_hash: api_hash.clone(), + params: InitParams { + reconnection_policy: &RetryPolicy, + ..Default::default() + }, + }) + .await?) +} + +/// Prompts the user for input. +/// +/// This function displays a message to the user and waits for input from standard input. +/// +/// # Arguments +/// +/// * `message` - The message to display to the user. +/// ``` +pub fn prompt(message: &str) -> Result { + let stdout = std::io::stdout(); + let mut stdout = stdout.lock(); + stdout.write_all(message.as_bytes())?; + stdout.flush()?; + + let stdin = std::io::stdin(); + let mut stdin = stdin.lock(); + + let mut line = String::new(); + stdin.read_line(&mut line)?; + Ok(line) +} + +/// Authorizes the Telegram client. +/// +/// This function checks if the client is authorized. If not, it prompts the user for +/// their phone number, requests a login code, and prompts the user for the received code. +/// If a password is required, it prompts the user for the password. Finally, it saves +/// the session to a file. +/// +/// # Arguments +/// +/// * `client` - The Telegram client to authorize. +/// * `session_file` - The path to the session file. +/// * `sign_out` - A mutable reference to a boolean indicating whether to sign out. +/// +/// # Errors +/// +/// This function will return an error if any of the following occurs: +/// * Prompting the user for input fails. +/// * Invoking a Grammers client method fails. +/// * Authorizing the client with phone number fails. +/// * Authorizing the client with a password fails. +pub async fn authorize_client( + client: &grammers_client::Client, + session_file: &str, + sign_out: &mut bool, +) -> Result<(), AuthorizationError> { + if !client.is_authorized().await? { + let phone = prompt("Enter your phone number (international format): ")?; + let token = client.request_login_code(&phone).await?; + let code = prompt("Enter the code you received: ")?; + let signed_in = client.sign_in(&token, &code).await; + match signed_in { + Err(SignInError::PasswordRequired(password_token)) => { + // Note: this `prompt` method will echo the password in the console. + // Real code might want to use a better way to handle this. + let hint = password_token.hint().unwrap_or("None"); + let prompt_message = format!("Enter the password (hint {}): ", &hint); + let password = prompt(prompt_message.as_str())?; + + client + .check_password(password_token, password.trim()) + .await + .map_err(Box::new)?; + } + Ok(_) => (), + Err(e) => panic!("{}", e), + }; + match client.session().save_to_file(session_file) { + Ok(_) => {} + Err(e) => { + tracing::warn!("NOTE: failed to save the session, will sign out when done: {e}"); + *sign_out = true; + } + } + } + + Ok(()) +} + +/// Filters a message based on the provided Telegram channel ID. +/// +/// This function iterates through the provided `filters` and checks if the `channel_id` matches any of the filters' Telegram channel IDs. +/// If a match is found, it returns the matching `Filter`. +/// +/// # Arguments +/// +/// * `channel_id` - The Telegram channel ID to be checked. +/// * `filters` - A slice of `Filter` objects to be checked against. +/// +/// # Errors +/// +/// This function does not return any errors. +pub fn filter_message(channel_id: i64, filters: &[Filter]) -> Option { + for filter in filters { + if let Some(filter_channel_id) = filter.telegram_channel_id { + if channel_id == filter_channel_id { + return Some(filter.clone()); + } + } + } + None +} diff --git a/token-scraper-core/src/util.rs b/token-scraper-core/src/util.rs index b1d9393..15d88aa 100644 --- a/token-scraper-core/src/util.rs +++ b/token-scraper-core/src/util.rs @@ -209,6 +209,72 @@ pub async fn get_token_price_jup(token: &str, vs_token: &str) -> Result Result { + /// The supply of pumpfun tokens. + const PUMPFUN_TOKEN_SUPPLY: u128 = 1_000_000_000; + + let price = get_token_price_jup(token, "USDC").await?; + let market_cap = (price * PUMPFUN_TOKEN_SUPPLY as f64) as u128; + Ok(market_cap) +} + +/// Filters tokens based on their market cap. +/// +/// This function fetches the market cap of a token and compares it to a given threshold. +/// If the market cap is below the threshold, it returns `false`. If the market cap is above +/// the threshold or if the token price is not found, it returns `true`. +/// +/// # Arguments +/// +/// * `token` - A string slice that holds the token symbol. +/// * `mc_threshold` - The market cap threshold. +/// +/// # Errors +/// +/// Returns a `MarketCapError` if the request to fetch the token price fails. +pub async fn filter_market_cap(token: &str, mc_threshold: u128) -> Result { + let market_cap_res = get_market_cap(token).await; + match market_cap_res { + Ok(market_cap) => { + if mc_threshold <= market_cap { + Ok(true) + } else { + tracing::info!( + "Skipping because market cap is above threshold: {}", + market_cap + ); + Ok(false) + } + } + Err(MarketCapError::GetTokenPriceJup(JupPriceApiError::PriceNotFound(resp))) => { + tracing::debug!("Price not found from Jupiter API: {}", resp); + tracing::debug!("Proceeding with sniper request"); + Ok(true) + } + Err(err) => Err(err), + } +} + #[cfg(test)] mod tests { use super::*; @@ -254,7 +320,7 @@ mod tests { async fn test_get_token_price_jup() { // Test with a valid token and vs_token let price = - get_token_price_jup("CT6sgK6Yz6LyfnSnY3PhS2VdvD2tFYkazPrNZEhNpump", "USDC").await; + get_token_price_jup("JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN", "USDC").await; assert!(price.is_ok(), "{}", price.err().unwrap()); }