diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3cb0f0b..ff60d2b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -62,8 +62,10 @@ jobs: run: | cargo test --no-fail-fast --lib -- --nocapture cargo test --no-fail-fast --test integration -- --nocapture --test-threads 1 + cargo test --no-fail-fast --test jupiter -- --nocapture --test-threads 1 env: TEST_DEVNET_RPC_ENDPOINT: ${{ secrets.DEVNET_RPC_ENDPOINT }} TEST_MAINNET_RPC_ENDPOINT: ${{ secrets.MAINNET_RPC_ENDPOINT }} TEST_PRIVATE_KEY: ${{ secrets.TEST_PRIVATE_KEY }} + TEST_MAINNET_PRIVATE_KEY: ${{ secrets.TEST_MAINNET_PRIVATE_KEY }} TEST_GRPC_X_TOKEN: ${{ secrets.TEST_GRPC_X_TOKEN }} diff --git a/Cargo.lock b/Cargo.lock index 32bf7aa..3409e45 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -109,6 +109,17 @@ dependencies = [ "zeroize", ] +[[package]] +name = "ahash" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" +dependencies = [ + "getrandom 0.2.15", + "once_cell", + "version_check", +] + [[package]] name = "ahash" version = "0.8.11" @@ -723,6 +734,18 @@ dependencies = [ "serde", ] +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + [[package]] name = "blake3" version = "1.6.1" @@ -869,6 +892,28 @@ dependencies = [ "serde", ] +[[package]] +name = "bytecheck" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23cdc57ce23ac53c931e88a43d06d070a6fd142f2617be5855eb75efc9beb1c2" +dependencies = [ + "bytecheck_derive", + "ptr_meta", + "simdutf8", +] + +[[package]] +name = "bytecheck_derive" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3db406d29fbcd95542e92559bed4d8ad92636d1ca8b3b72ede10b4bcc010e659" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "bytemuck" version = "1.22.0" @@ -1325,7 +1370,7 @@ name = "drift-rs" version = "1.0.0-alpha.15" dependencies = [ "abi_stable", - "ahash", + "ahash 0.8.11", "anchor-lang", "arrayvec", "base64 0.22.1", @@ -1338,6 +1383,7 @@ dependencies = [ "futures-util", "hex", "hex-literal", + "jupiter-swap-api-client", "log", "rayon", "regex", @@ -1349,6 +1395,7 @@ dependencies = [ "solana-rpc-client-api", "solana-sdk", "solana-transaction-status", + "spl-associated-token-account", "thiserror 1.0.69", "tokio", "tokio-stream", @@ -1571,6 +1618,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + [[package]] name = "futures" version = "0.3.31" @@ -1781,6 +1834,9 @@ name = "hashbrown" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash 0.7.8", +] [[package]] name = "hashbrown" @@ -1788,7 +1844,7 @@ version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" dependencies = [ - "ahash", + "ahash 0.8.11", ] [[package]] @@ -2002,6 +2058,23 @@ dependencies = [ "tokio-rustls 0.24.1", ] +[[package]] +name = "hyper-rustls" +version = "0.27.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d191583f3da1305256f22463b9bb0471acad48a4e534a5218b9963e9c1f59b2" +dependencies = [ + "futures-util", + "http 1.2.0", + "hyper 1.6.0", + "hyper-util", + "rustls 0.23.25", + "rustls-pki-types", + "tokio", + "tokio-rustls 0.26.2", + "tower-service", +] + [[package]] name = "hyper-timeout" version = "0.5.2" @@ -2015,11 +2088,27 @@ dependencies = [ "tower-service", ] +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper 1.6.0", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + [[package]] name = "hyper-util" -version = "0.1.11" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "497bbc33a26fdd4af9ed9c70d63f61cf56a938375fbb32df34db9b1cd6d643f2" +checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4" dependencies = [ "bytes", "futures-channel", @@ -2027,7 +2116,6 @@ dependencies = [ "http 1.2.0", "http-body 1.0.1", "hyper 1.6.0", - "libc", "pin-project-lite", "socket2", "tokio", @@ -2339,6 +2427,23 @@ dependencies = [ "serde_json", ] +[[package]] +name = "jupiter-swap-api-client" +version = "0.1.0" +source = "git+https://github.com/drift-labs/jupiter-swap-api-client#b699edf30ff1f561460579ee2504622e28f9a797" +dependencies = [ + "anyhow", + "base64 0.22.1", + "reqwest 0.12.15", + "rust_decimal", + "serde", + "serde_json", + "serde_qs", + "solana-account-decoder", + "solana-sdk", + "thiserror 2.0.12", +] + [[package]] name = "keccak" version = "0.1.5" @@ -2971,6 +3076,26 @@ dependencies = [ "autotools", ] +[[package]] +name = "ptr_meta" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1" +dependencies = [ + "ptr_meta_derive", +] + +[[package]] +name = "ptr_meta_derive" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "qstring" version = "0.7.2" @@ -2989,6 +3114,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + [[package]] name = "rand" version = "0.7.3" @@ -3148,6 +3279,15 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +[[package]] +name = "rend" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71fe3824f5629716b1589be05dacd749f6aa084c87e00e016714a8cdfccc997c" +dependencies = [ + "bytecheck", +] + [[package]] name = "repr_offset" version = "0.2.2" @@ -3173,7 +3313,7 @@ dependencies = [ "http 0.2.12", "http-body 0.4.6", "hyper 0.14.32", - "hyper-rustls", + "hyper-rustls 0.24.2", "ipnet", "js-sys", "log", @@ -3188,7 +3328,7 @@ dependencies = [ "serde_json", "serde_urlencoded", "sync_wrapper 0.1.2", - "system-configuration", + "system-configuration 0.5.1", "tokio", "tokio-rustls 0.24.1", "tokio-util", @@ -3201,6 +3341,50 @@ dependencies = [ "winreg", ] +[[package]] +name = "reqwest" +version = "0.12.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d19c46a6fdd48bc4dab94b6103fccc55d34c67cc0ad04653aad4ea2a07cd7bbb" +dependencies = [ + "base64 0.22.1", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2 0.4.9", + "http 1.2.0", + "http-body 1.0.1", + "http-body-util", + "hyper 1.6.0", + "hyper-rustls 0.27.5", + "hyper-tls", + "hyper-util", + "ipnet", + "js-sys", + "log", + "mime", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls-pemfile 2.2.0", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper 1.0.2", + "system-configuration 0.6.1", + "tokio", + "tokio-native-tls", + "tower 0.5.2", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows-registry", +] + [[package]] name = "reqwest-middleware" version = "0.2.5" @@ -3210,7 +3394,7 @@ dependencies = [ "anyhow", "async-trait", "http 0.2.12", - "reqwest", + "reqwest 0.11.27", "serde", "task-local-extensions", "thiserror 1.0.69", @@ -3230,6 +3414,51 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rkyv" +version = "0.7.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9008cd6385b9e161d8229e1f6549dd23c3d022f132a2ea37ac3a10ac4935779b" +dependencies = [ + "bitvec", + "bytecheck", + "bytes", + "hashbrown 0.12.3", + "ptr_meta", + "rend", + "rkyv_derive", + "seahash", + "tinyvec", + "uuid", +] + +[[package]] +name = "rkyv_derive" +version = "0.7.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "503d1d27590a2b0a3a4ca4c94755aa2875657196ecbf401a42eff41d7de532c0" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "rust_decimal" +version = "1.37.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faa7de2ba56ac291bd90c6b9bece784a52ae1411f9506544b3eae36dd2356d50" +dependencies = [ + "arrayvec", + "borsh 1.5.5", + "bytes", + "num-traits", + "rand 0.8.5", + "rkyv", + "serde", + "serde_json", +] + [[package]] name = "rustc-demangle" version = "0.1.24" @@ -3272,9 +3501,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.26" +version = "0.23.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df51b5869f3a441595eac5e8ff14d486ff285f7b8c0df8770e49c3b56351f0f0" +checksum = "822ee9188ac4ec04a2f0531e55d035fb2de73f18b41a63c70c2712503b6fb13c" dependencies = [ "log", "once_cell", @@ -3379,6 +3608,12 @@ dependencies = [ "untrusted", ] +[[package]] +name = "seahash" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" + [[package]] name = "security-framework" version = "2.11.1" @@ -3471,6 +3706,17 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_qs" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd34f36fe4c5ba9654417139a9b3a20d2e1de6012ee678ad14d240c22c78d8d6" +dependencies = [ + "percent-encoding", + "serde", + "thiserror 1.0.69", +] + [[package]] name = "serde_spanned" version = "0.6.8" @@ -3591,6 +3837,12 @@ version = "1.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c" +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + [[package]] name = "siphasher" version = "0.3.11" @@ -4058,7 +4310,7 @@ version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89e1d3b52b4a014efeaaab67f14e40af3972a4be61c523d612860db8e3145529" dependencies = [ - "ahash", + "ahash 0.8.11", "lazy_static", "solana-epoch-schedule", "solana-hash", @@ -4744,7 +4996,7 @@ dependencies = [ "bs58", "indicatif", "log", - "reqwest", + "reqwest 0.11.27", "reqwest-middleware", "semver", "serde", @@ -4780,7 +5032,7 @@ dependencies = [ "base64 0.22.1", "bs58", "jsonrpc-core", - "reqwest", + "reqwest 0.11.27", "reqwest-middleware", "semver", "serde", @@ -5844,6 +6096,9 @@ name = "sync_wrapper" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] [[package]] name = "synstructure" @@ -5864,7 +6119,18 @@ checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" dependencies = [ "bitflags 1.3.2", "core-foundation 0.9.4", - "system-configuration-sys", + "system-configuration-sys 0.5.0", +] + +[[package]] +name = "system-configuration" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags 2.9.0", + "core-foundation 0.9.4", + "system-configuration-sys 0.6.0", ] [[package]] @@ -5877,6 +6143,22 @@ dependencies = [ "libc", ] +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + [[package]] name = "task-local-extensions" version = "0.1.4" @@ -6029,7 +6311,7 @@ version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" dependencies = [ - "rustls 0.23.26", + "rustls 0.23.25", "tokio", ] @@ -6206,6 +6488,7 @@ dependencies = [ "futures-util", "pin-project-lite", "sync_wrapper 1.0.2", + "tokio", "tower-layer", "tower-service", ] @@ -6398,6 +6681,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "uuid" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9" + [[package]] name = "vcpkg" version = "0.2.15" @@ -6589,6 +6878,35 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6dccfd733ce2b1753b03b6d3c65edf020262ea35e20ccdf3e288043e6dd620e3" +[[package]] +name = "windows-registry" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3" +dependencies = [ + "windows-result", + "windows-strings", + "windows-targets 0.53.0", +] + +[[package]] +name = "windows-result" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06374efe858fab7e4f881500e6e86ec8bc28f9462c47e5a9941a0142ad86b189" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-sys" version = "0.48.0" @@ -6640,13 +6958,29 @@ dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm", + "windows_i686_gnullvm 0.52.6", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", "windows_x86_64_msvc 0.52.6", ] +[[package]] +name = "windows-targets" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1e4c7e8ceaaf9cb7d7507c974735728ab453b67ef8f18febdd7c11fe59dca8b" +dependencies = [ + "windows_aarch64_gnullvm 0.53.0", + "windows_aarch64_msvc 0.53.0", + "windows_i686_gnu 0.53.0", + "windows_i686_gnullvm 0.53.0", + "windows_i686_msvc 0.53.0", + "windows_x86_64_gnu 0.53.0", + "windows_x86_64_gnullvm 0.53.0", + "windows_x86_64_msvc 0.53.0", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" @@ -6659,6 +6993,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" + [[package]] name = "windows_aarch64_msvc" version = "0.48.5" @@ -6671,6 +7011,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" + [[package]] name = "windows_i686_gnu" version = "0.48.5" @@ -6683,12 +7029,24 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" +[[package]] +name = "windows_i686_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" + [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" + [[package]] name = "windows_i686_msvc" version = "0.48.5" @@ -6701,6 +7059,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_i686_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" + [[package]] name = "windows_x86_64_gnu" version = "0.48.5" @@ -6713,6 +7077,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" + [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" @@ -6725,6 +7095,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" + [[package]] name = "windows_x86_64_msvc" version = "0.48.5" @@ -6737,6 +7113,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" + [[package]] name = "winnow" version = "0.7.3" @@ -6777,6 +7159,15 @@ version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + [[package]] name = "yellowstone-grpc-client" version = "6.0.0" diff --git a/Cargo.toml b/Cargo.toml index 2976552..4cc8b50 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,6 +35,8 @@ dashmap = "6" env_logger = "0.11" futures-util = "0.3" hex = "0.4" +# TODO: use jup-ag version once solana crate versions relaxed +jupiter-swap-api-client = { git = "https://github.com/drift-labs/jupiter-swap-api-client", package = "jupiter-swap-api-client" } log = "0.4" rayon = { version = "1.9.0", optional = true } regex = "1.10" @@ -45,6 +47,7 @@ solana-rpc-client = "2.1" solana-rpc-client-api = "2.1" solana-sdk = "2.1" solana-transaction-status = "2.1" +spl-associated-token-account = "6.0" thiserror = "1" tokio = { version = "1.42", features = ["full"] } tokio-stream = "0.1.17" diff --git a/crates/src/constants.rs b/crates/src/constants.rs index a1f737f..60a133f 100644 --- a/crates/src/constants.rs +++ b/crates/src/constants.rs @@ -4,7 +4,7 @@ use solana_sdk::{address_lookup_table::AddressLookupTableAccount, pubkey::Pubkey use crate::{ drift_idl::accounts::{PerpMarket, SpotMarket}, - types::Context, + types::{accounts::State, Context}, MarketId, MarketType, OracleSource, }; @@ -25,11 +25,22 @@ pub const JIT_PROXY_ID: Pubkey = /// Empty pubkey pub const DEFAULT_PUBKEY: Pubkey = solana_sdk::pubkey!("11111111111111111111111111111111"); +pub const SYSTEM_PROGRAM_ID: Pubkey = DEFAULT_PUBKEY; + static STATE_ACCOUNT: OnceLock = OnceLock::new(); +/// Address of the SPL Token program pub const TOKEN_PROGRAM_ID: Pubkey = solana_sdk::pubkey!("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"); +/// Address of the SPL Token 2022 program +pub const TOKEN_2022_PROGRAM_ID: Pubkey = + solana_sdk::pubkey!("TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb"); + +/// Address of Associated Token Program +pub const ASSOCIATED_TOKEN_PROGRAM_ID: Pubkey = + solana_sdk::pubkey!("ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL"); + /// Drift market lookup table (DevNet) pub const LUTS_DEVNET: &[Pubkey] = &[solana_sdk::pubkey!( "FaMS3U4uBojvGn5FSDEPimddcXsCfwkKsFgMVVnDdxGb" @@ -117,6 +128,8 @@ pub struct ProgramData { spot_markets: &'static [SpotMarket], perp_markets: &'static [PerpMarket], pub lookup_tables: &'static [AddressLookupTableAccount], + // drift state account + state: State, } impl ProgramData { @@ -126,6 +139,7 @@ impl ProgramData { spot_markets: &[], perp_markets: &[], lookup_tables: &[], + state: unsafe { std::mem::zeroed() }, } } /// Initialize `ProgramData` @@ -133,6 +147,7 @@ impl ProgramData { mut spot: Vec, mut perp: Vec, lookup_tables: Vec, + state: State, ) -> Self { spot.sort_by(|a, b| a.market_index.cmp(&b.market_index)); perp.sort_by(|a, b| a.market_index.cmp(&b.market_index)); @@ -154,9 +169,17 @@ impl ProgramData { spot_markets: Box::leak(spot.into_boxed_slice()), perp_markets: Box::leak(perp.into_boxed_slice()), lookup_tables: Box::leak(lookup_tables.into_boxed_slice()), + state, } } + /// Return drift `State` account (cached) + /// + /// prefer live + pub fn state(&self) -> &State { + &self.state + } + /// Return known spot markets pub fn spot_market_configs(&self) -> &'static [SpotMarket] { self.spot_markets @@ -168,11 +191,15 @@ impl ProgramData { } /// Return the spot market config given a market index + /// + /// Useful for static metadata e.g. token program address pub fn spot_market_config_by_index(&self, market_index: u16) -> Option<&'static SpotMarket> { self.spot_markets.get(market_index as usize) } /// Return the perp market config given a market index + /// + /// Useful for static metadata e.g. token program address pub fn perp_market_config_by_index(&self, market_index: u16) -> Option<&'static PerpMarket> { self.perp_markets.get(market_index as usize) } diff --git a/crates/src/jupiter.rs b/crates/src/jupiter.rs new file mode 100644 index 0000000..61ba6d4 --- /dev/null +++ b/crates/src/jupiter.rs @@ -0,0 +1,149 @@ +//! Jupiter SDK helpers +pub use jupiter_swap_api_client::{ + quote::{QuoteResponse, SwapMode}, + swap::SwapInstructionsResponse, + transaction_config::TransactionConfig, + JupiterSwapApiClient, +}; +use solana_sdk::{message::AddressLookupTableAccount, pubkey::Pubkey}; + +use crate::{ + types::{SdkError, SdkResult}, + utils, DriftClient, +}; + +/// Default Jupiter API url +const DEFAULT_JUPITER_API_URL: &str = "https://lite-api.jup.ag/swap/v1"; + +/// jupiter swap IXs and metadata for building a swap Tx +pub struct JupiterSwapInfo { + pub quote: QuoteResponse, + pub ixs: SwapInstructionsResponse, + pub luts: Vec, +} + +pub trait JupiterSwapApi { + fn jupiter_swap_query( + &self, + user_authority: &Pubkey, + amount: u64, + swap_mode: SwapMode, + in_market: u16, + out_market: u16, + slippage_bps: u16, + only_direct_routes: Option, + excluded_dexes: Option, + transaction_config: Option, + ) -> impl std::future::Future> + Send; +} + +impl JupiterSwapApi for DriftClient { + /// Fetch Jupiter swap ixs and metadata for a token swap + /// + /// This function queries Jupiter API to get the optimal swap route and corresponding instructions + /// for swapping between two tokens. + /// + /// # Arguments + /// + /// * `rpc` - A Solana RPC client + /// * `user_authority` - The public key of the user's wallet that will execute the swap + /// * `amount` - The amount of input tokens to swap, in native units (smallest denomination) + /// * `swap_mode` - The type of swap to perform (e.g. ExactIn, ExactOut) + /// * `in_market` - The market index of the token to swap from + /// * `out_market` - The market index of the token to swap to + /// * `slippage_bps` - Maximum allowed slippage in basis points (1 bp = 0.01%) + /// * `only_direct_routes` - If Some(true), only consider direct swap routes between the tokens + /// * `excluded_dexes` - Optional comma-separated string of DEX names to exclude from routing + /// * `transaction_config` - Optional configuration for the swap transaction + /// + /// # Returns + /// + /// Returns a `Result` containing `JupiterSwapInfo` with the swap instructions and route details + /// if successful, or a `SdkError` if the operation fails. + /// + /// # Example + /// + /// ```no_run + /// use solana_sdk::pubkey::Pubkey; + /// + /// let swap_info = jupiter_swap_query( + /// rpc_client, + /// user_wallet.pubkey(), + /// 1_000_000, // 1 USDC + /// SwapMode::ExactIn, + /// 50, // 0.5% slippage + /// usdc_mint, + /// sol_mint, + /// Some(true), + /// None, + /// None + /// ).await?; + /// ``` + async fn jupiter_swap_query( + &self, + user_authority: &Pubkey, + amount: u64, + swap_mode: SwapMode, + slippage_bps: u16, + in_market: u16, + out_market: u16, + only_direct_routes: Option, + excluded_dexes: Option, + transaction_config: Option, + ) -> SdkResult { + let jupiter_url = + std::env::var("JUPITER_API_URL").unwrap_or(DEFAULT_JUPITER_API_URL.into()); + let jup_client = JupiterSwapApiClient::new(jupiter_url); + + let in_market = self.try_get_spot_market_account(in_market)?; + let out_market = self.try_get_spot_market_account(out_market)?; + + // GET /quote + let quote_request = jupiter_swap_api_client::quote::QuoteRequest { + amount, + swap_mode: Some(swap_mode), + input_mint: in_market.mint, + output_mint: out_market.mint, + slippage_bps, + only_direct_routes, + excluded_dexes, + ..Default::default() + }; + + let quote_response = jup_client.quote("e_request).await.map_err(|err| { + log::error!("jupiter api request: {err:?}"); + SdkError::Generic(err.to_string()) + })?; + // POST /swap-instructions + let swap_instructions = jup_client + .swap_instructions(&jupiter_swap_api_client::swap::SwapRequest { + user_public_key: *user_authority, + quote_response: quote_response.clone(), + config: transaction_config.unwrap_or_default(), + }) + .await + .map_err(|err| { + log::error!("jupiter api request: {err:?}"); + SdkError::Generic(err.to_string()) + })?; + + let res = self + .rpc() + .get_multiple_accounts(swap_instructions.address_lookup_table_addresses.as_slice()) + .await?; + + let luts = res + .iter() + .zip(swap_instructions.address_lookup_table_addresses.iter()) + .map(|(acc, key)| { + utils::deserialize_alt(*key, acc.as_ref().expect("deser LUT")).expect("deser LUT") + }) + .collect(); + + Ok(JupiterSwapInfo { + luts, + quote: quote_response, + ixs: swap_instructions, + }) + } +} diff --git a/crates/src/lib.rs b/crates/src/lib.rs index 7f9ca85..3c259b2 100644 --- a/crates/src/lib.rs +++ b/crates/src/lib.rs @@ -8,11 +8,17 @@ use std::{ }; use anchor_lang::{AccountDeserialize, Discriminator, InstructionData}; +use constants::{ + ASSOCIATED_TOKEN_PROGRAM_ID, SYSTEM_PROGRAM_ID, TOKEN_2022_PROGRAM_ID, TOKEN_PROGRAM_ID, +}; pub use drift_pubsub_client::PubsubClient; use futures_util::TryFutureExt; use log::debug; pub use solana_rpc_client::nonblocking::rpc_client::RpcClient; -use solana_rpc_client_api::response::Response; +use solana_rpc_client_api::{ + config::RpcSimulateTransactionConfig, + response::{Response, RpcSimulateTransactionResult}, +}; use solana_sdk::{ account::Account, clock::Slot, @@ -34,6 +40,7 @@ use crate::{ }, drift_idl::traits::ToAccountMetas, grpc::grpc_subscriber::{AccountFilter, DriftGrpcClient, GeyserSubscribeOpts}, + jupiter::JupiterSwapInfo, marketmap::MarketMap, oraclemap::{Oracle, OracleMap}, swift_order_subscriber::{SignedOrderInfo, SwiftOrderStream}, @@ -48,6 +55,7 @@ pub use crate::{grpc::GrpcSubscribeOpts, types::Context, wallet::Wallet}; // utils pub mod async_utils; pub mod ffi; +pub mod jupiter; pub mod math; pub mod memcmp; pub mod utils; @@ -476,6 +484,29 @@ impl DriftClient { self.backend.try_get_account(state_account()) } + /// Simulate the tx on remote RPC node + pub async fn simulate_tx( + &self, + tx: VersionedMessage, + ) -> SdkResult { + let response = self + .rpc() + .simulate_transaction_with_config( + &VersionedTransaction { + message: tx, + // must provide a signature for the RPC call to work + signatures: vec![Signature::new_unique()], + }, + RpcSimulateTransactionConfig { + sig_verify: false, + replace_recent_blockhash: true, + ..Default::default() + }, + ) + .await; + response.map(|r| r.value).map_err(Into::into) + } + /// Sign and send a tx to the network /// /// Returns the signature on success @@ -780,6 +811,15 @@ impl DriftClientBackend { let lut_pubkeys = context.luts(); + let account_map = AccountMap::new( + Arc::clone(&pubsub_client), + Arc::clone(&rpc_client), + rpc_client.commitment(), + ); + account_map + .subscribe_account_polled(state_account(), Some(Duration::from_secs(180))) + .await?; + let (_, _, lut_accounts) = tokio::try_join!( perp_market_map.sync(&rpc_client), spot_market_map.sync(&rpc_client), @@ -813,14 +853,6 @@ impl DriftClientBackend { all_oracles.as_slice(), rpc_client.commitment(), ); - let account_map = AccountMap::new( - Arc::clone(&pubsub_client), - Arc::clone(&rpc_client), - rpc_client.commitment(), - ); - account_map - .subscribe_account_polled(state_account(), Some(Duration::from_secs(180))) - .await?; Ok(Self { rpc_client: Arc::clone(&rpc_client), @@ -830,6 +862,7 @@ impl DriftClientBackend { spot_market_map.values(), perp_market_map.values(), lookup_tables, + account_map.account_data(state_account()).unwrap(), ), account_map, perp_market_map, @@ -1345,9 +1378,7 @@ impl<'a> TransactionBuilder<'a> { } /// Extend the tx lookup tables (always includes the defacto drift LUTs) pub fn lookup_tables(mut self, lookup_tables: &[AddressLookupTableAccount]) -> Self { - self.lookup_tables = lookup_tables.to_vec(); - self.lookup_tables - .extend(self.program_data.lookup_tables.iter().cloned()); + self.lookup_tables.extend_from_slice(lookup_tables); self } @@ -1374,7 +1405,7 @@ impl<'a> TransactionBuilder<'a> { pub fn deposit( mut self, amount: u64, - spot_market_index: u16, + market_index: u16, user_token_account: Pubkey, reduce_only: Option, ) -> Self { @@ -1385,20 +1416,24 @@ impl<'a> TransactionBuilder<'a> { user: self.sub_account, user_stats: Wallet::derive_stats_account(&self.authority), authority: self.authority, - spot_market_vault: constants::derive_spot_market_vault(spot_market_index), + spot_market_vault: constants::derive_spot_market_vault(market_index), user_token_account, - token_program: constants::TOKEN_PROGRAM_ID, + token_program: self + .program_data + .spot_market_config_by_index(market_index) + .unwrap() + .token_program(), }, &[self.account_data.as_ref()], self.force_markets.readable.iter(), - [MarketId::spot(spot_market_index)].iter(), + [MarketId::spot(market_index)].iter(), ); let ix = Instruction { program_id: constants::PROGRAM_ID, accounts, data: InstructionData::data(&drift_idl::instructions::Deposit { - market_index: spot_market_index, + market_index, amount, reduce_only: reduce_only.unwrap_or(false), }), @@ -1413,7 +1448,7 @@ impl<'a> TransactionBuilder<'a> { pub fn withdraw( mut self, amount: u64, - spot_market_index: u16, + market_index: u16, user_token_account: Pubkey, reduce_only: Option, ) -> Self { @@ -1424,14 +1459,18 @@ impl<'a> TransactionBuilder<'a> { user: self.sub_account, user_stats: Wallet::derive_stats_account(&self.authority), authority: self.authority, - spot_market_vault: constants::derive_spot_market_vault(spot_market_index), + spot_market_vault: constants::derive_spot_market_vault(market_index), user_token_account, drift_signer: constants::derive_drift_signer(), - token_program: constants::TOKEN_PROGRAM_ID, + token_program: self + .program_data + .spot_market_config_by_index(market_index) + .unwrap() + .token_program(), }, &[self.account_data.as_ref()], self.force_markets.readable.iter(), - [MarketId::spot(spot_market_index)] + [MarketId::spot(market_index)] .iter() .chain(self.force_markets.writeable.iter()), ); @@ -1440,7 +1479,7 @@ impl<'a> TransactionBuilder<'a> { program_id: constants::PROGRAM_ID, accounts, data: InstructionData::data(&drift_idl::instructions::Withdraw { - market_index: spot_market_index, + market_index, amount, reduce_only: reduce_only.unwrap_or(false), }), @@ -1943,7 +1982,7 @@ impl<'a> TransactionBuilder<'a> { self.program_data, types::accounts::UpdateUserCustomMarginRatio { authority: self.authority, - user: Wallet::derive_user_account(&self.authority, sub_account_id), + user: self.sub_account, }, &[self.account_data.as_ref()], std::iter::empty(), @@ -1962,6 +2001,214 @@ impl<'a> TransactionBuilder<'a> { self } + /// Add a spot `begin_swap` ix + /// + /// This should be followed by a subsequent `end_swap` ix + pub fn begin_swap( + mut self, + amount_in: u64, + in_market: &SpotMarket, + out_market: &SpotMarket, + payer_token_account: &Pubkey, + payee_token_account: &Pubkey, + ) -> Self { + let in_token_program = in_market.token_program(); + let out_token_program = out_market.token_program(); + + let mut accounts = build_accounts( + self.program_data, + types::accounts::BeginSwap { + state: *state_account(), + user: self.sub_account, + user_stats: Wallet::derive_stats_account(&self.authority), + authority: self.authority, + out_spot_market_vault: out_market.vault, + in_spot_market_vault: in_market.vault, + in_token_account: *payer_token_account, + out_token_account: *payee_token_account, + token_program: in_token_program, + drift_signer: self.program_data.state().signer, + instructions: SYSVAR_INSTRUCTIONS_PUBKEY, + }, + &[self.account_data.as_ref()], + [MarketId::QUOTE_SPOT].iter(), + [ + MarketId::spot(in_market.market_index), + MarketId::spot(out_market.market_index), + ] + .iter(), + ); + + if out_token_program != in_token_program { + accounts.push(AccountMeta::new_readonly(out_token_program, false)); + } + + if out_market.token_program == 1 || in_market.token_program == 1 { + accounts.push(AccountMeta::new_readonly(in_market.mint, false)); + accounts.push(AccountMeta::new_readonly(out_market.mint, false)); + } + + let ix = Instruction { + program_id: constants::PROGRAM_ID, + accounts, + data: InstructionData::data(&drift_idl::instructions::BeginSwap { + in_market_index: in_market.market_index, + out_market_index: out_market.market_index, + amount_in, + }), + }; + self.ixs.push(ix); + + self + } + + /// Add a spot `end_swap` ix + /// + /// This should follow a preceding `begin_swap` ix + pub fn end_swap( + mut self, + in_market: &SpotMarket, + out_market: &SpotMarket, + payer_token_account: &Pubkey, + payee_token_account: &Pubkey, + limit_price: Option, + reduce_only: Option, + ) -> Self { + let in_token_program = in_market.token_program(); + + let accounts = build_accounts( + self.program_data, + types::accounts::EndSwap { + state: *state_account(), + user: self.sub_account, + user_stats: Wallet::derive_stats_account(&self.authority), + authority: self.authority, + out_spot_market_vault: out_market.vault, + in_spot_market_vault: in_market.vault, + in_token_account: *payer_token_account, + out_token_account: *payee_token_account, + token_program: in_token_program, + drift_signer: self.program_data.state().signer, + instructions: SYSVAR_INSTRUCTIONS_PUBKEY, + }, + &[self.account_data.as_ref()], + [MarketId::QUOTE_SPOT].iter(), + [ + MarketId::spot(in_market.market_index), + MarketId::spot(out_market.market_index), + ] + .iter(), + ); + + let ix = Instruction { + program_id: constants::PROGRAM_ID, + accounts, + data: InstructionData::data(&drift_idl::instructions::EndSwap { + in_market_index: in_market.market_index, + out_market_index: out_market.market_index, + limit_price, + reduce_only, + }), + }; + self.ixs.push(ix); + + self + } + + /// Add a Jupiter token swap to the tx + /// + /// # Arguments + /// * `jupiter_swap_info` - Jupiter swap route and instructions + /// * `in_market` - Spot market of the input token + /// * `out_market` - Spot market of the output token + /// * `in_token_account` - Input token account pubkey + /// * `out_token_account` - Output token account pubkey + /// * `limit_price` - Set a limit price + /// * `reduce_only` - Set a reduce only order + pub fn jupiter_swap( + mut self, + jupiter_swap_info: JupiterSwapInfo, + in_market: &SpotMarket, + out_market: &SpotMarket, + in_token_account: &Pubkey, + out_token_account: &Pubkey, + limit_price: Option, + reduce_only: Option, + ) -> Self { + let jupiter_swap_ixs = jupiter_swap_info.ixs; + + // initialize token accounts + if !jupiter_swap_ixs.setup_instructions.is_empty() { + // jupiter swap ixs imply account creation is required + // provide our own creation ixs + // new_self.ixs.extend(jupiter_swap_ixs.setup_instructions); + + // TODO: support alternative payer address e.g. delegate + let create_in_account_ix = Instruction { + program_id: ASSOCIATED_TOKEN_PROGRAM_ID, + accounts: vec![ + AccountMeta::new(self.authority, true), // payer + AccountMeta::new(*in_token_account, false), + AccountMeta::new_readonly(self.authority, false), // wallet + AccountMeta::new_readonly(in_market.mint, false), + AccountMeta::new_readonly(SYSTEM_PROGRAM_ID, false), + AccountMeta::new_readonly(in_market.token_program(), false), + ], + data: vec![1], // idempotent mode + }; + let create_out_account_ix = Instruction { + program_id: ASSOCIATED_TOKEN_PROGRAM_ID, + accounts: vec![ + AccountMeta::new(self.authority, true), // payer + AccountMeta::new(*out_token_account, false), + AccountMeta::new_readonly(self.authority, false), // wallet + AccountMeta::new_readonly(out_market.mint, false), + AccountMeta::new_readonly(SYSTEM_PROGRAM_ID, false), + AccountMeta::new_readonly(out_market.token_program(), false), + ], + data: vec![1], // idempotent mode + }; + self.ixs + .extend_from_slice(&[create_in_account_ix, create_out_account_ix]); + } + + let mut new_self = self.begin_swap( + jupiter_swap_info.quote.in_amount, + in_market, + out_market, + in_token_account, + out_token_account, + ); + + // TODO: support jito bundle + if !jupiter_swap_ixs.other_instructions.is_empty() { + panic!("jupiter swap unsupported ix: Jito tip"); + } + + new_self.ixs.push(jupiter_swap_ixs.swap_instruction); + + // support SOL unwrap ixs, ignore account delete/reclaim ixs + if let Some(unwrap_ix) = jupiter_swap_ixs.cleanup_instruction { + if unwrap_ix.program_id != TOKEN_PROGRAM_ID + && unwrap_ix.program_id != TOKEN_2022_PROGRAM_ID + { + new_self.ixs.push(unwrap_ix); + } + } + + new_self = new_self.end_swap( + in_market, + out_market, + in_token_account, + out_token_account, + limit_price, + reduce_only, + ); + + // Add the jup tx LUTs + new_self.lookup_tables(&jupiter_swap_info.luts) + } + /// Build the transaction message ready for signing and sending pub fn build(self) -> VersionedMessage { if self.legacy { diff --git a/crates/src/types.rs b/crates/src/types.rs index 46c0e3e..e08ae4e 100644 --- a/crates/src/types.rs +++ b/crates/src/types.rs @@ -28,12 +28,14 @@ pub use crate::drift_idl::{ types::*, }; use crate::{ - constants::{ids, LUTS_DEVNET, LUTS_MAINNET}, + constants::{ids, LUTS_DEVNET, LUTS_MAINNET, TOKEN_2022_PROGRAM_ID, TOKEN_PROGRAM_ID}, drift_idl::errors::ErrorCode, grpc::grpc_subscriber::GrpcError, Wallet, }; +use self::accounts::SpotMarket; + /// Map from K => V pub type MapOf = DashMap; @@ -46,6 +48,17 @@ pub fn is_one_of_variant(value: &T, variants: &[T]) -> bool { variants.iter().any(|variant| value == variant) } +impl SpotMarket { + /// Return the spot market's token program address + pub fn token_program(&self) -> Pubkey { + if self.token_program == 1 { + TOKEN_2022_PROGRAM_ID + } else { + TOKEN_PROGRAM_ID + } + } +} + /// Drift program context /// /// Contains network specific variables necessary for interacting with drift program diff --git a/crates/src/utils.rs b/crates/src/utils.rs index 757c1fc..f2923be 100644 --- a/crates/src/utils.rs +++ b/crates/src/utils.rs @@ -152,6 +152,12 @@ pub mod test_envs { let private_key = std::env::var("TEST_PRIVATE_KEY").expect("TEST_PRIVATE_KEY set"); Keypair::from_base58_string(private_key.as_str()) } + /// keypair for mainnet integration tests + pub fn mainnet_test_keypair() -> Keypair { + let private_key = + std::env::var("TEST_MAINNET_PRIVATE_KEY").expect("TEST_MAINNET_PRIVATE_KEY set"); + Keypair::from_base58_string(private_key.as_str()) + } } /// copy of `solana_sdk::ed25519_instruction::Ed25519SignatureOffsets` diff --git a/crates/src/wallet.rs b/crates/src/wallet.rs index dca7d62..863371d 100644 --- a/crates/src/wallet.rs +++ b/crates/src/wallet.rs @@ -11,7 +11,7 @@ use solana_sdk::{ use crate::{ constants::{self}, - types::{SdkError, SdkResult}, + types::{accounts::SpotMarket, SdkError, SdkResult}, utils, }; @@ -159,6 +159,15 @@ impl Wallet { account_drift_pda } + /// Calculate the wallet's ATA for drift spot market + pub fn derive_associated_token_address(authority: &Pubkey, market: &SpotMarket) -> Pubkey { + spl_associated_token_account::get_associated_token_address_with_program_id( + authority, + &market.mint, + &market.token_program(), + ) + } + /// Signs a solana message (ixs, accounts) and builds a signed tx /// ready for sending over RPC /// diff --git a/tests/integration.rs b/tests/integration.rs index ef4588f..4b78c6b 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -208,7 +208,6 @@ async fn place_and_cancel_orders() { let result = client.sign_and_send(tx).await; dbg!(&result); assert!(result.is_ok()); - client.unsubscribe().await.unwrap(); } #[ignore] diff --git a/tests/jupiter.rs b/tests/jupiter.rs new file mode 100644 index 0000000..20763f0 --- /dev/null +++ b/tests/jupiter.rs @@ -0,0 +1,295 @@ +use drift_rs::{ + event_subscriber::RpcClient, + jupiter::{JupiterSwapApi, SwapMode}, + types::{accounts::User, Context, MarketId}, + utils::test_envs::{mainnet_endpoint, mainnet_test_keypair}, + DriftClient, TransactionBuilder, Wallet, +}; +use solana_sdk::{ + instruction::InstructionError, native_token::LAMPORTS_PER_SOL, transaction::TransactionError, +}; +use tokio::sync::OnceCell; + +const DRIFT_CLIENT: OnceCell = OnceCell::const_new(); + +async fn drift_client() -> DriftClient { + DRIFT_CLIENT + .get_or_init(|| async move { + let wallet: Wallet = mainnet_test_keypair().into(); + DriftClient::new( + Context::MainNet, + RpcClient::new(mainnet_endpoint()), + wallet.clone(), + ) + .await + .unwrap() + }) + .await + .clone() +} + +#[tokio::test] +async fn jupiter_swap_exact_in_udsc_to_sol() { + let _ = env_logger::try_init(); + let client = drift_client().await; + let wallet = client.wallet(); + + let token_in = MarketId::QUOTE_SPOT; + let token_out = MarketId::spot(1); + + let user: User = client + .get_user_account(&wallet.default_sub_account()) + .await + .expect("exists"); + + let jupiter_swap_info = client + .jupiter_swap_query( + wallet.authority(), + 10_000_000, + SwapMode::ExactIn, + 10, + token_in.index(), + token_out.index(), + Some(true), + None, + None, + ) + .await + .expect("got jup swap ixs"); + + let in_market = client + .program_data() + .spot_market_config_by_index(token_in.index()) + .unwrap(); + let out_market = client + .program_data() + .spot_market_config_by_index(token_out.index()) + .unwrap(); + + let in_token_account = Wallet::derive_associated_token_address(&wallet.authority(), &in_market); + let out_token_account = + Wallet::derive_associated_token_address(&wallet.authority(), &out_market); + + let tx = TransactionBuilder::new( + client.program_data(), + wallet.default_sub_account(), + std::borrow::Cow::Borrowed(&user), + false, + ) + .jupiter_swap( + jupiter_swap_info, + &in_market, + &out_market, + &in_token_account, + &out_token_account, + None, + None, + ) + .build(); + + let result = client.simulate_tx(tx).await; + dbg!(&result); + assert!(result.expect("sim ok").err.is_none()); +} + +#[tokio::test] +async fn jupiter_swap_exact_out_udsc_to_sol() { + let _ = env_logger::try_init(); + let client = drift_client().await; + let wallet = client.wallet(); + + let token_in = MarketId::QUOTE_SPOT; + let token_out = MarketId::spot(1); + + let user: User = client + .get_user_account(&wallet.default_sub_account()) + .await + .expect("exists"); + + let jupiter_swap_info = client + .jupiter_swap_query( + wallet.authority(), + 1 * LAMPORTS_PER_SOL, + SwapMode::ExactOut, + 10, + token_in.index(), + token_out.index(), + Some(true), + None, + None, + ) + .await + .expect("got jup swap ixs"); + + let in_market = client + .program_data() + .spot_market_config_by_index(token_in.index()) + .unwrap(); + let out_market = client + .program_data() + .spot_market_config_by_index(token_out.index()) + .unwrap(); + + let in_token_account = Wallet::derive_associated_token_address(&wallet.authority(), &in_market); + let out_token_account = + Wallet::derive_associated_token_address(&wallet.authority(), &out_market); + + let tx = TransactionBuilder::new( + client.program_data(), + wallet.default_sub_account(), + std::borrow::Cow::Borrowed(&user), + false, + ) + .jupiter_swap( + jupiter_swap_info, + &in_market, + &out_market, + &in_token_account, + &out_token_account, + None, + None, + ) + .build(); + + let result = client.simulate_tx(tx).await; + dbg!(&result); + assert!(result.expect("sim ok").err.is_none()); +} + +#[tokio::test] +async fn jupiter_swap_exact_out_udsc_jto() { + let _ = env_logger::try_init(); + let client = drift_client().await; + let wallet = client.wallet(); + + let token_in = MarketId::QUOTE_SPOT; + let token_out = client.market_lookup("JTO").unwrap(); + + let in_market = client + .program_data() + .spot_market_config_by_index(token_in.index()) + .unwrap(); + let out_market = client + .program_data() + .spot_market_config_by_index(token_out.index()) + .unwrap(); + + let user: User = client + .get_user_account(&wallet.default_sub_account()) + .await + .expect("exists"); + + let jupiter_swap_info = client + .jupiter_swap_query( + wallet.authority(), + 5 * 10_u64.pow(out_market.decimals), + SwapMode::ExactOut, + 10, + token_in.index(), + token_out.index(), + Some(true), + None, + None, + ) + .await + .expect("got jup swap ixs"); + + let in_token_account = Wallet::derive_associated_token_address(&wallet.authority(), &in_market); + let out_token_account = + Wallet::derive_associated_token_address(&wallet.authority(), &out_market); + + let tx = TransactionBuilder::new( + client.program_data(), + wallet.default_sub_account(), + std::borrow::Cow::Borrowed(&user), + false, + ) + .jupiter_swap( + jupiter_swap_info, + &in_market, + &out_market, + &in_token_account, + &out_token_account, + None, + None, + ) + .build(); + + let result = client.simulate_tx(tx).await; + dbg!(&result); + assert!(result.expect("sim ok").err.is_none()); +} + +#[tokio::test] +async fn jupiter_swap_sol_unwrap() { + let _ = env_logger::try_init(); + let client = drift_client().await; + let wallet = client.wallet(); + + let token_in = client.market_lookup("SOL").unwrap(); + let token_out = client.market_lookup("mSOL").unwrap(); + + let in_market = client + .program_data() + .spot_market_config_by_index(token_in.index()) + .unwrap(); + let out_market = client + .program_data() + .spot_market_config_by_index(token_out.index()) + .unwrap(); + + let user: User = client + .get_user_account(&wallet.default_sub_account()) + .await + .expect("exists"); + + let jupiter_swap_info = client + .jupiter_swap_query( + wallet.authority(), + 1 * LAMPORTS_PER_SOL, + SwapMode::ExactIn, + 10, + token_in.index(), + token_out.index(), + Some(true), + None, + None, + ) + .await + .expect("got jup swap ixs"); + + let in_token_account = Wallet::derive_associated_token_address(&wallet.authority(), &in_market); + let out_token_account = + Wallet::derive_associated_token_address(&wallet.authority(), &out_market); + + let tx = TransactionBuilder::new( + client.program_data(), + wallet.default_sub_account(), + std::borrow::Cow::Borrowed(&user), + false, + ) + .jupiter_swap( + jupiter_swap_info, + &in_market, + &out_market, + &in_token_account, + &out_token_account, + None, + None, + ) + .build(); + + let result = client.simulate_tx(tx).await; + dbg!(&result); + let err = result.expect("sim ok").err; + // either swap OK or it would incur borrow which is fine (test account missing 'token in' amount) + match err { + Some(err) => { + assert_eq!( + err, + TransactionError::InstructionError(4, InstructionError::Custom(6157)) + ) + } + None => assert!(true), + } +}