diff --git a/Cargo.lock b/Cargo.lock index 3439d4a..10b588d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -415,7 +415,7 @@ dependencies = [ "anchor-lang 0.30.1", "spl-associated-token-account 3.0.4", "spl-pod 0.2.5", - "spl-token", + "spl-token 4.0.0", "spl-token-2022 3.0.4", "spl-token-group-interface 0.2.5", "spl-token-metadata-interface 0.3.5", @@ -535,6 +535,21 @@ version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" +[[package]] +name = "anyhow_ext" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a135cb522bf5b2254ed712979bc242f60c13f7906c1e4585d5fef36ae9017528" +dependencies = [ + "anyhow", +] + +[[package]] +name = "arc-swap" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" + [[package]] name = "ark-bn254" version = "0.4.0" @@ -888,6 +903,12 @@ dependencies = [ "rustc-demangle", ] +[[package]] +name = "base58" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6107fe1be6682a68940da878d9e9f5e90ca5745b3dec9fd1bb393c8777d4f581" + [[package]] name = "base64" version = "0.12.3" @@ -906,6 +927,12 @@ version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "base64ct" version = "1.6.0" @@ -1013,7 +1040,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4114279215a005bc675e386011e594e1d9b800918cea18fcadadcce864a2046b" dependencies = [ "borsh-derive 0.10.3", - "hashbrown 0.12.3", + "hashbrown 0.13.2", ] [[package]] @@ -1699,6 +1726,26 @@ dependencies = [ "subtle", ] +[[package]] +name = "dirs" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + [[package]] name = "displaydoc" version = "0.2.5" @@ -1875,6 +1922,16 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +[[package]] +name = "erased-serde" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24e2389d65ab4fab27dc2a5de7b191e1f6617d1f1c8855c0dc569c94a4cbb18d" +dependencies = [ + "serde", + "typeid", +] + [[package]] name = "errno" version = "0.3.9" @@ -1897,16 +1954,20 @@ dependencies = [ "bincode", "bytemuck", "bytes", + "chrono", "clap 4.5.9", "crossbeam", + "dirs", "env_logger 0.11.3", "fixed", "fixed-macro", "futures", "futures-sink", + "hex", "jito-protos", "jito-searcher-client", "jupiter-swap-api-client", + "lazy_static", "log", "marginfi", "num-traits", @@ -1921,12 +1982,15 @@ dependencies = [ "solana-rpc-client-api", "solana-sdk", "spl-associated-token-account 2.3.0", - "spl-token", + "spl-token 4.0.0", + "switchboard-on-demand", + "switchboard-on-demand-client", "thiserror", "tokio", "toml 0.8.15", "tonic", "tonic-health", + "url", "yellowstone-grpc-client", "yellowstone-grpc-proto", ] @@ -2605,7 +2669,7 @@ source = "git+https://github.com/mrgnlabs/jito-rs?branch=1.18.17#380213171ef847d dependencies = [ "bincode", "bytes", - "prost", + "prost 0.12.6", "prost-types", "solana-perf", "solana-sdk", @@ -2700,6 +2764,16 @@ version = "0.2.155" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" +[[package]] +name = "libredox" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +dependencies = [ + "bitflags 2.6.0", + "libc", +] + [[package]] name = "libsecp256k1" version = "0.6.0" @@ -2710,15 +2784,34 @@ dependencies = [ "base64 0.12.3", "digest 0.9.0", "hmac-drbg", - "libsecp256k1-core", - "libsecp256k1-gen-ecmult", - "libsecp256k1-gen-genmult", + "libsecp256k1-core 0.2.2", + "libsecp256k1-gen-ecmult 0.2.1", + "libsecp256k1-gen-genmult 0.2.1", "rand 0.7.3", "serde", "sha2 0.9.9", "typenum", ] +[[package]] +name = "libsecp256k1" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95b09eff1b35ed3b33b877ced3a691fc7a481919c7e29c53c906226fcf55e2a1" +dependencies = [ + "arrayref", + "base64 0.13.1", + "digest 0.9.0", + "hmac-drbg", + "libsecp256k1-core 0.3.0", + "libsecp256k1-gen-ecmult 0.3.0", + "libsecp256k1-gen-genmult 0.3.0", + "rand 0.8.5", + "serde", + "sha2 0.9.9", + "typenum", +] + [[package]] name = "libsecp256k1-core" version = "0.2.2" @@ -2730,13 +2823,33 @@ dependencies = [ "subtle", ] +[[package]] +name = "libsecp256k1-core" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be9b9bb642d8522a44d533eab56c16c738301965504753b03ad1de3425d5451" +dependencies = [ + "crunchy", + "digest 0.9.0", + "subtle", +] + [[package]] name = "libsecp256k1-gen-ecmult" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccab96b584d38fac86a83f07e659f0deafd0253dc096dab5a36d53efe653c5c3" dependencies = [ - "libsecp256k1-core", + "libsecp256k1-core 0.2.2", +] + +[[package]] +name = "libsecp256k1-gen-ecmult" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3038c808c55c87e8a172643a7d87187fc6c4174468159cb3090659d55bcb4809" +dependencies = [ + "libsecp256k1-core 0.3.0", ] [[package]] @@ -2745,7 +2858,16 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67abfe149395e3aa1c48a2beb32b068e2334402df8181f818d3aee2b304c4f5d" dependencies = [ - "libsecp256k1-core", + "libsecp256k1-core 0.2.2", +] + +[[package]] +name = "libsecp256k1-gen-genmult" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3db8d6ba2cec9eacc40e6e8ccc98931840301f1006e95647ceb2dd5c3aa06f7c" +dependencies = [ + "libsecp256k1-core 0.3.0", ] [[package]] @@ -2781,11 +2903,14 @@ name = "log" version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +dependencies = [ + "value-bag", +] [[package]] name = "marginfi" version = "0.1.0" -source = "git+https://github.com/mrgnlabs/marginfi-v2?branch=main#e48a838a77470025a3dde8bc19624a1f986a5d13" +source = "git+https://github.com/mrgnlabs/marginfi-v2?branch=man0s/crossbar-legacy-indexer#d6f25279df16c8f745fba331d37967ac0df3571d" dependencies = [ "anchor-lang 0.29.0", "anchor-lang 0.30.1", @@ -2804,6 +2929,7 @@ dependencies = [ "spl-tlv-account-resolution 0.6.5", "spl-transfer-hook-interface 0.6.5", "static_assertions", + "switchboard-on-demand", "switchboard-solana", "type-layout", ] @@ -3104,6 +3230,15 @@ dependencies = [ "libc", ] +[[package]] +name = "num_enum" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f646caf906c20226733ed5b1374287eb97e3c2a5c227ce668c1f2ce20ae57c9" +dependencies = [ + "num_enum_derive 0.5.11", +] + [[package]] name = "num_enum" version = "0.6.1" @@ -3122,6 +3257,18 @@ dependencies = [ "num_enum_derive 0.7.2", ] +[[package]] +name = "num_enum_derive" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcbff9bc912032c62bf65ef1d5aea88983b420f4f839db1e9b0c281a25c9c799" +dependencies = [ + "proc-macro-crate 1.3.1", + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "num_enum_derive" version = "0.6.1" @@ -3140,7 +3287,7 @@ version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "681030a937600a36906c185595136d26abfebb4aa9c65701cefcaf8578bb982b" dependencies = [ - "proc-macro-crate 1.3.1", + "proc-macro-crate 3.1.0", "proc-macro2", "quote", "syn 2.0.58", @@ -3261,6 +3408,16 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "pbjson" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7e6349fa080353f4a597daffd05cb81572a9c031a6d4fff7e504947496fcc68" +dependencies = [ + "base64 0.21.7", + "serde", +] + [[package]] name = "pbkdf2" version = "0.4.0" @@ -3476,7 +3633,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "deb1435c188b76130da55f17a466d252ff7b1418b2ad3e037d127b94e3411f29" dependencies = [ "bytes", - "prost-derive", + "prost-derive 0.12.6", +] + +[[package]] +name = "prost" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b2ecbe40f08db5c006b5764a2645f7f3f141ce756412ac9e1dd6087e6d32995" +dependencies = [ + "bytes", + "prost-derive 0.13.2", ] [[package]] @@ -3493,7 +3660,7 @@ dependencies = [ "once_cell", "petgraph", "prettyplease", - "prost", + "prost 0.12.6", "prost-types", "regex", "syn 2.0.58", @@ -3513,13 +3680,26 @@ dependencies = [ "syn 2.0.58", ] +[[package]] +name = "prost-derive" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acf0c195eebb4af52c752bec4f52f645da98b6e92077a04110c7f349477ae5ac" +dependencies = [ + "anyhow", + "itertools 0.12.1", + "proc-macro2", + "quote", + "syn 2.0.58", +] + [[package]] name = "prost-types" version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9091c90b0a32608e984ff2fa4091273cbdd755d54935c51d520887f4a1dbd5b0" dependencies = [ - "prost", + "prost 0.12.6", ] [[package]] @@ -3820,6 +4000,17 @@ dependencies = [ "bitflags 2.6.0", ] +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom 0.2.15", + "libredox", + "thiserror", +] + [[package]] name = "regex" version = "1.10.5" @@ -4242,6 +4433,15 @@ dependencies = [ "syn 2.0.58", ] +[[package]] +name = "serde_fmt" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d4ddca14104cd60529e8c7f7ba71a2c8acd8f7f5cfcdc2faf97eeb7c3010a4" +dependencies = [ + "serde", +] + [[package]] name = "serde_json" version = "1.0.120" @@ -4458,7 +4658,7 @@ dependencies = [ "serde_json", "solana-config-program", "solana-sdk", - "spl-token", + "spl-token 4.0.0", "spl-token-2022 1.0.0", "spl-token-group-interface 0.1.0", "spl-token-metadata-interface 0.2.0", @@ -4726,7 +4926,7 @@ dependencies = [ "js-sys", "lazy_static", "libc", - "libsecp256k1", + "libsecp256k1 0.6.0", "light-poseidon", "log", "memoffset 0.9.1", @@ -4946,7 +5146,7 @@ dependencies = [ "itertools 0.10.5", "js-sys", "lazy_static", - "libsecp256k1", + "libsecp256k1 0.6.0", "log", "memmap2", "num-derive 0.4.2", @@ -5088,7 +5288,7 @@ dependencies = [ "solana-sdk", "spl-associated-token-account 2.3.0", "spl-memo", - "spl-token", + "spl-token 4.0.0", "spl-token-2022 1.0.0", "thiserror", ] @@ -5227,7 +5427,7 @@ dependencies = [ "num-derive 0.4.2", "num-traits", "solana-program", - "spl-token", + "spl-token 4.0.0", "spl-token-2022 1.0.0", "thiserror", ] @@ -5243,7 +5443,7 @@ dependencies = [ "num-derive 0.4.2", "num-traits", "solana-program", - "spl-token", + "spl-token 4.0.0", "spl-token-2022 3.0.4", "thiserror", ] @@ -5431,6 +5631,21 @@ dependencies = [ "spl-type-length-value 0.4.6", ] +[[package]] +name = "spl-token" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e85e168a785e82564160dcb87b2a8e04cee9bfd1f4d488c729d53d6a4bd300d" +dependencies = [ + "arrayref", + "bytemuck", + "num-derive 0.3.3", + "num-traits", + "num_enum 0.5.11", + "solana-program", + "thiserror", +] + [[package]] name = "spl-token" version = "4.0.0" @@ -5462,7 +5677,7 @@ dependencies = [ "solana-zk-token-sdk", "spl-memo", "spl-pod 0.1.0", - "spl-token", + "spl-token 4.0.0", "spl-token-group-interface 0.1.0", "spl-token-metadata-interface 0.2.0", "spl-transfer-hook-interface 0.4.1", @@ -5486,7 +5701,7 @@ dependencies = [ "solana-zk-token-sdk", "spl-memo", "spl-pod 0.2.5", - "spl-token", + "spl-token 4.0.0", "spl-token-group-interface 0.2.5", "spl-token-metadata-interface 0.3.5", "spl-transfer-hook-interface 0.6.5", @@ -5642,6 +5857,154 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab16ced94dbd8a46c82fd81e3ed9a8727dac2977ea869d217bcc4ea1f122e81f" +[[package]] +name = "sval" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53eb957fbc79a55306d5d25d87daf3627bc3800681491cda0709eef36c748bfe" + +[[package]] +name = "sval_buffer" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96e860aef60e9cbf37888d4953a13445abf523c534640d1f6174d310917c410d" +dependencies = [ + "sval", + "sval_ref", +] + +[[package]] +name = "sval_dynamic" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea3f2b07929a1127d204ed7cb3905049381708245727680e9139dac317ed556f" +dependencies = [ + "sval", +] + +[[package]] +name = "sval_fmt" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4e188677497de274a1367c4bda15bd2296de4070d91729aac8f0a09c1abf64d" +dependencies = [ + "itoa", + "ryu", + "sval", +] + +[[package]] +name = "sval_json" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32f456c07dae652744781f2245d5e3b78e6a9ebad70790ac11eb15dbdbce5282" +dependencies = [ + "itoa", + "ryu", + "sval", +] + +[[package]] +name = "sval_nested" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "886feb24709f0476baaebbf9ac10671a50163caa7e439d7a7beb7f6d81d0a6fb" +dependencies = [ + "sval", + "sval_buffer", + "sval_ref", +] + +[[package]] +name = "sval_ref" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be2e7fc517d778f44f8cb64140afa36010999565528d48985f55e64d45f369ce" +dependencies = [ + "sval", +] + +[[package]] +name = "sval_serde" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79bf66549a997ff35cd2114a27ac4b0c2843280f2cfa84b240d169ecaa0add46" +dependencies = [ + "serde", + "sval", + "sval_nested", +] + +[[package]] +name = "switchboard-common" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96fe58be35530580b729fa5d846661c89a007982527f4ff0ca6010168564159" +dependencies = [ + "base64 0.21.7", + "hex", + "log", + "serde", + "serde_json", + "sha2 0.10.8", + "sha3 0.10.8", +] + +[[package]] +name = "switchboard-on-demand" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3852951c42f8876a443060b6882bda945f1621224236ead37959e80f5369cf81" +dependencies = [ + "arc-swap", + "async-trait", + "base64 0.21.7", + "bincode", + "borsh 0.10.3", + "bytemuck", + "futures", + "lazy_static", + "libsecp256k1 0.7.1", + "log", + "num 0.4.3", + "rust_decimal", + "serde", + "serde_json", + "sha2 0.10.8", + "solana-address-lookup-table-program", + "solana-program", + "spl-associated-token-account 2.3.0", + "spl-token 3.5.0", + "switchboard-common", +] + +[[package]] +name = "switchboard-on-demand-client" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8a4ee6334ce2e60bd5c4e0c9b0ba0269d31d17e76c7f546683be55622c16c0e" +dependencies = [ + "anyhow_ext", + "arrayref", + "base58", + "base64 0.22.1", + "borsh 1.5.1", + "bs58 0.4.0", + "bytemuck", + "futures", + "hex", + "lazy_static", + "pbjson", + "prost 0.13.2", + "reqwest", + "rust_decimal", + "serde", + "serde_json", + "sha2 0.10.8", + "solana-client", + "solana-sdk", +] + [[package]] name = "switchboard-solana" version = "0.29.110" @@ -6037,7 +6400,7 @@ dependencies = [ "hyper-timeout", "percent-encoding", "pin-project", - "prost", + "prost 0.12.6", "rustls", "rustls-native-certs", "rustls-pemfile", @@ -6071,7 +6434,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f80db390246dfb46553481f6024f0082ba00178ea495dbb99e70ba9a4fafb5e1" dependencies = [ "async-stream", - "prost", + "prost 0.12.6", "tokio", "tokio-stream", "tonic", @@ -6189,6 +6552,12 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "typeid" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e13db2e0ccd5e14a544e8a246ba2312cd25223f616442d7f2cb0e3db614236e" + [[package]] name = "typenum" version = "1.17.0" @@ -6304,6 +6673,42 @@ version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314" +[[package]] +name = "value-bag" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a84c137d37ab0142f0f2ddfe332651fdbf252e7b7dbb4e67b6c1f1b2e925101" +dependencies = [ + "value-bag-serde1", + "value-bag-sval2", +] + +[[package]] +name = "value-bag-serde1" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccacf50c5cb077a9abb723c5bcb5e0754c1a433f1e1de89edc328e2760b6328b" +dependencies = [ + "erased-serde", + "serde", + "serde_fmt", +] + +[[package]] +name = "value-bag-sval2" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1785bae486022dfb9703915d42287dcb284c1ee37bd1080eeba78cc04721285b" +dependencies = [ + "sval", + "sval_buffer", + "sval_dynamic", + "sval_fmt", + "sval_json", + "sval_ref", + "sval_serde", +] + [[package]] name = "vcpkg" version = "0.2.15" @@ -6703,7 +7108,7 @@ source = "git+https://github.com/mrgnlabs/yellowstone-grpc?branch=1.18.17#0ec7e2 dependencies = [ "anyhow", "bincode", - "prost", + "prost 0.12.6", "protobuf-src", "solana-account-decoder", "solana-sdk", diff --git a/Cargo.toml b/Cargo.toml index a197835..5a4c281 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,14 +21,16 @@ bytemuck = "1.14.0" bytes = "1.5.0" clap = { version = "4.5.4", features = ["derive"] } crossbeam = { version = "0.8.4", features = ["crossbeam-channel"] } +dirs = "4.0.0" env_logger = "0.11.3" fixed = "1.24.0" fixed-macro = "1.2.0" futures = "0.3.30" futures-sink = "0.3.30" jupiter-swap-api-client = "0.1.0" +lazy_static = "1.5.0" log = "0.4.21" -marginfi = { git = "https://github.com/mrgnlabs/marginfi-v2", branch = "main", features = [ +marginfi = { git = "https://github.com/mrgnlabs/marginfi-v2", branch = "man0s/crossbar-legacy-indexer", features = [ "mainnet-beta", "client", "no-entrypoint", @@ -56,6 +58,11 @@ yellowstone-grpc-client = { git = "https://github.com/mrgnlabs/yellowstone-grpc" yellowstone-grpc-proto = { git = "https://github.com/mrgnlabs/yellowstone-grpc", branch = "1.18.17" } jito-protos = { git = "https://github.com/mrgnlabs/jito-rs", branch = "1.18.17" } jito-searcher-client = { git = "https://github.com/mrgnlabs/jito-rs", branch = "1.18.17" } +switchboard-on-demand = "0.1.7" +switchboard-on-demand-client = "0.1.7" +chrono = "0.4.38" +hex = "0.4.3" +url = "2.5.2" [profile.release] opt-level = 3 diff --git a/Dockerfile b/Dockerfile index 3c86fb1..860b30a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -27,5 +27,7 @@ WORKDIR /app # Copy the build artifact from the build stage COPY --from=builder /usr/src/app/target/release/eva01 . +ENV RUST_LOG=eva01=info + # Set the startup command -CMD ["./eva01", "run", "/config/config.toml"] \ No newline at end of file +CMD ["./eva01", "run", "/config/config.toml"] diff --git a/src/cli/app.rs b/src/cli/app.rs index aac4e3d..294f4db 100644 --- a/src/cli/app.rs +++ b/src/cli/app.rs @@ -31,7 +31,7 @@ pub struct SetupFromCliOpts { #[arg(short = 'u', long, help = "RPC endpoint url")] pub rpc_url: String, #[arg(short = 'k', long, help = "Signer keypair path")] - pub keypair_path: String, + pub keypair_path: PathBuf, #[arg( long, help = "Signer pubkey, if not provided, will be derived from the keypair" diff --git a/src/cli/setup/mod.rs b/src/cli/setup/mod.rs index b1e3197..90d1180 100644 --- a/src/cli/setup/mod.rs +++ b/src/cli/setup/mod.rs @@ -1,75 +1,96 @@ -use crate::config::{Eva01Config, GeneralConfig, LiquidatorCfg, RebalancerCfg}; +use super::app::SetupFromCliOpts; +use crate::{ + config::{Eva01Config, GeneralConfig, LiquidatorCfg, RebalancerCfg}, + utils::{ask_keypair_until_valid, expand_tilde, is_valid_url, prompt_user}, +}; + +use anyhow::bail; use fixed::types::I80F48; +use lazy_static::lazy_static; use solana_account_decoder::{UiAccountEncoding, UiDataSliceConfig}; -use solana_client::rpc_client::RpcClient; use solana_client::{ + rpc_client::RpcClient, rpc_config::{RpcAccountInfoConfig, RpcProgramAccountsConfig}, rpc_filter::{Memcmp, MemcmpEncodedBytes, RpcFilterType}, }; use solana_program::pubkey::Pubkey; -use solana_sdk::{ - signature::{read_keypair_file, Signer}, - signer::keypair::Keypair, -}; -use std::io::Write; -use std::path::PathBuf; - -use super::app::SetupFromCliOpts; +use solana_sdk::signature::{read_keypair_file, Signer}; +use std::{ops::Not, path::PathBuf, str::FromStr}; /// Helper for initializing Marginfi Account pub mod initialize; -/// 1º -> Ask for path where the config file will be stored -/// -/// 2º -> Ask for a solana RPC endpoint url -/// -> Verify if we have access to the RPC endpoint -/// -/// 3º -> Ask for the signer keypair path -/// -> Check if the MarginfiAccount is already initialized in that keypair -/// If not, try to initialize a account -/// -/// 4º -> Ask for the yellowstone rpc and optinal x token +lazy_static! { + static ref DEFAULT_CONFIG_PATH: PathBuf = { + let mut path = dirs::home_dir().expect("Couldn't find the config directory"); + path.push(".config"); + path.push("eva01"); + + path + }; +} + pub async fn setup() -> anyhow::Result<()> { - // 1º Step - let configuration_path = PathBuf::from( - prompt_user("Pretended configuration file location\nExample: /home/mrgn/.config/liquidator/config.toml\n> ")? - ); + // Config location + let input_raw = prompt_user(&format!( + "Select config location [default: {:?}]: ", + *DEFAULT_CONFIG_PATH + ))?; + let configuration_dir = if input_raw.is_empty() { + DEFAULT_CONFIG_PATH.clone() + } else { + expand_tilde(&input_raw) + }; + if !configuration_dir.exists() { + std::fs::create_dir_all(&configuration_dir)?; + } + let configuration_path = configuration_dir.join("config.toml"); - // 2º Step - let rpc_url = prompt_user("RPC endpoint url\n> ")?; + // RPC config + let rpc_url = prompt_user("RPC endpoint url [required]: ")?; + if !is_valid_url(&rpc_url) { + bail!("Invalid RPC endpoint"); + } let rpc_client = RpcClient::new(rpc_url.clone()); - // 3º Step + // Target program/group + let input_raw = prompt_user(&format!( + "Select marginfi program [default: {:?}]: ", + GeneralConfig::default_marginfi_program_id() + ))?; + let marginfi_program_id = if input_raw.is_empty() { + GeneralConfig::default_marginfi_program_id() + } else { + Pubkey::from_str(&input_raw).expect("Invalid marginfi program id") + }; + + let input_raw = prompt_user(&format!( + "Select marginfi group [default: {:?}]: ", + GeneralConfig::default_marginfi_group_address() + ))?; + let marginfi_group_address = if input_raw.is_empty() { + GeneralConfig::default_marginfi_group_address() + } else { + Pubkey::from_str(&input_raw).expect("Invalid marginfi group address") + }; + + // Marginfi account discovery/selection let (keypair_path, signer_keypair) = ask_keypair_until_valid()?; let accounts = marginfi_account_by_authority(signer_keypair.pubkey(), rpc_client).await?; if accounts.is_empty() { - let create_new = - prompt_user("There is no marginfi account \nDo you wish to create a new one? Y/n\n> ")? - .as_str() - != "n"; - if !create_new { - println!("Can't proceed without a marginfi account."); - return Err(anyhow::anyhow!("Can't proceed without a marginfi account.")); - } - // Initialize a marginfi account + println!("No marginfi account found for the provided signer. Please create one first."); + bail!("No marginfi account found"); + // TODO: initialize a marginfi account programmatically } - // 4º step - let yellowstone_endpoint = prompt_user("Yellowstone endpoint url\n> ")?; - + let yellowstone_endpoint = prompt_user("Yellowstone endpoint url [required]: ")?; let yellowstone_x_token = { - let x_token = - prompt_user("Do you wish to add yellowstone x token? \nPress enter if not\n> ")?; - - if x_token.is_empty() { - None - } else { - Some(x_token) - } + let x_token = prompt_user("Yellowstone x-token [optional]: ")?; + x_token.is_empty().not().then_some(x_token) }; let isolated_banks = - prompt_user("Do you wish to liquidate on isolated banks? Y/n\n> ")?.to_lowercase() == "y"; + prompt_user("Enable isolated banks liquidation? [Y/n] ")?.to_lowercase() == "y"; let general_config = GeneralConfig { rpc_url, @@ -81,8 +102,8 @@ pub async fn setup() -> anyhow::Result<()> { liquidator_account: accounts[0], compute_unit_price_micro_lamports: GeneralConfig::default_compute_unit_price_micro_lamports( ), - marginfi_program_id: GeneralConfig::default_marginfi_program_id(), - marginfi_group_address: GeneralConfig::default_marginfi_group_address(), + marginfi_program_id, + marginfi_group_address, account_whitelist: GeneralConfig::default_account_whitelist(), address_lookup_tables: GeneralConfig::default_address_lookup_tables(), }; @@ -152,7 +173,7 @@ pub async fn setup_from_cfg( Some(pubkey) => pubkey, None => { let signer_keypair = read_keypair_file(&keypair_path) - .unwrap_or_else(|_| panic!("Failed to read keypair from path: {}", keypair_path)); + .unwrap_or_else(|_| panic!("Failed to read keypair from path: {:?}", keypair_path)); signer_keypair.pubkey() } }; @@ -223,30 +244,6 @@ pub async fn setup_from_cfg( Ok(()) } -fn prompt_user(prompt_text: &str) -> anyhow::Result { - print!("{}", prompt_text); - let mut input = String::new(); - std::io::stdout().flush()?; - std::io::stdin().read_line(&mut input)?; - input.pop(); - Ok(input) -} - -/// Simply asks the keypair path until it is a valid one, -/// Returns (keypair_path, signer_keypair) -fn ask_keypair_until_valid() -> anyhow::Result<(String, Keypair)> { - println!("Keypair file path"); - loop { - let keypair_path = prompt_user("> ")?; - match read_keypair_file(&keypair_path) { - Ok(keypair) => return Ok((keypair_path, keypair)), - Err(_) => { - println!("Failed to load the keypair from the provided path. Please try again"); - } - } - } -} - async fn marginfi_account_by_authority( authority: Pubkey, rpc_client: RpcClient, diff --git a/src/config.rs b/src/config.rs index 48a0e3f..0023383 100644 --- a/src/config.rs +++ b/src/config.rs @@ -56,7 +56,7 @@ pub struct GeneralConfig { serialize_with = "pubkey_to_str" )] pub signer_pubkey: Pubkey, - pub keypair_path: String, + pub keypair_path: PathBuf, #[serde( deserialize_with = "from_pubkey_string", serialize_with = "pubkey_to_str" @@ -99,7 +99,7 @@ impl std::fmt::Display for GeneralConfig { - Yellowstone Endpoint: {}\n\ - Yellowstone X Token: {}\n\ - Signer Pubkey: {}\n\ - - Keypair Path: {}\n\ + - Keypair Path: {:?}\n\ - Liquidator Account: {}\n\ - Compute Unit Price Micro Lamports: {}\n\ - Marginfi Program ID: {}\n\ diff --git a/src/crossbar.rs b/src/crossbar.rs new file mode 100644 index 0000000..a901e63 --- /dev/null +++ b/src/crossbar.rs @@ -0,0 +1,70 @@ +use solana_sdk::pubkey::Pubkey; +use std::{ + collections::HashMap, + sync::{Arc, Mutex}, +}; +use switchboard_on_demand_client::CrossbarClient; + +/// CrossbarMaintainer will maintain the feeds prices +/// with simulated prices from the crossbar service +pub(crate) struct CrossbarMaintainer { + crossbar_client: CrossbarClient, +} + +impl CrossbarMaintainer { + /// Creates a new CrossbarMaintainer empty instance + pub fn new() -> Self { + let crossbar_client = CrossbarClient::default(None); + Self { crossbar_client } + } + + pub async fn simulate(&self, feeds: Vec<(Pubkey, String)>) -> Vec<(Pubkey, f64)> { + if feeds.is_empty() { + return Vec::new(); + } + + // Create a fast lookup map from feed hash to oracle hash + let feed_hash_to_oracle_hash_map: HashMap = feeds + .iter() + .map(|(address, feed_hash)| (feed_hash.clone(), address.clone())) + .collect(); + + let feed_hashes: Vec<&str> = feeds + .iter() + .map(|(_, feed_hash)| feed_hash.as_str()) + .collect(); + + let simulated_prices = self + .crossbar_client + .simulate_feeds(&feed_hashes) + .await + .unwrap(); + let mut prices = Vec::new(); + for simulated_response in simulated_prices { + if let Some(price) = calculate_price(simulated_response.results) { + prices.push(( + feed_hash_to_oracle_hash_map + .get(&simulated_response.feedHash) + .unwrap() + .clone(), + price, + )); + } + } + prices + } +} +fn calculate_price(mut numbers: Vec) -> Option { + if numbers.is_empty() { + return None; + } + + numbers.sort_by(|a, b| a.partial_cmp(b).unwrap()); + let mid = numbers.len() / 2; + + if numbers.len() % 2 == 0 { + Some((numbers[mid - 1] + numbers[mid]) / 2.0) + } else { + Some(numbers[mid]) + } +} diff --git a/src/geyser.rs b/src/geyser.rs index 3b5dd74..b702cd5 100644 --- a/src/geyser.rs +++ b/src/geyser.rs @@ -3,7 +3,7 @@ use anchor_lang::AccountDeserialize; use crossbeam::channel::Sender; use futures::StreamExt; use log::{error, info}; -use marginfi::{instructions::marginfi_account, state::marginfi_account::MarginfiAccount}; +use marginfi::state::marginfi_account::MarginfiAccount; use solana_program::pubkey::Pubkey; use solana_sdk::account::Account; use std::{collections::HashMap, mem::size_of}; @@ -170,7 +170,6 @@ impl GeyserService { } } } - Ok(()) } /// Builds a geyser subscription request payload diff --git a/src/liquidator.rs b/src/liquidator.rs index c4676eb..305194f 100644 --- a/src/liquidator.rs +++ b/src/liquidator.rs @@ -1,5 +1,6 @@ use crate::{ config::{GeneralConfig, LiquidatorCfg}, + crossbar::CrossbarMaintainer, geyser::{AccountType, GeyserUpdate}, transaction_manager::BatchTransactions, utils::{ @@ -16,13 +17,16 @@ use anchor_lang::Discriminator; use crossbeam::channel::{Receiver, Sender}; use fixed::types::I80F48; use fixed_macro::types::I80F48; -use log::{debug, error, info, warn}; +use log::{debug, error, info}; use marginfi::{ - constants::EXP_10_I80F48, + constants::{BANKRUPT_THRESHOLD, EXP_10_I80F48}, state::{ marginfi_account::{BalanceSide, MarginfiAccount, RequirementType}, - marginfi_group::{Bank, RiskTier}, - price::{OraclePriceFeedAdapter, OraclePriceType, PriceAdapter, PriceBias}, + marginfi_group::{Bank, BankOperationalState, RiskTier}, + price::{ + OraclePriceFeedAdapter, OraclePriceType, OracleSetup, PriceBias, + SwitchboardPullPriceFeed, + }, }, }; use rayon::prelude::*; @@ -41,6 +45,7 @@ use std::{ collections::HashMap, sync::{atomic::AtomicBool, Arc}, }; +use switchboard_on_demand::PullFeedAccountData; /// Bank group private key offset const BANK_GROUP_PK_OFFSET: usize = 32 + 1 + 8; @@ -55,6 +60,7 @@ pub struct Liquidator { banks: HashMap, oracle_to_bank: HashMap, stop_liquidation: Arc, + crossbar_client: CrossbarMaintainer, } #[derive(Clone)] @@ -121,6 +127,7 @@ impl Liquidator { liquidator_account, oracle_to_bank: HashMap::new(), stop_liquidation, + crossbar_client: CrossbarMaintainer::new(), } } @@ -146,17 +153,46 @@ impl Liquidator { match msg.account_type { AccountType::OracleAccount => { if let Some(bank_to_update_pk) = self.oracle_to_bank.get(&msg.address) { - let oracle_ai = (&msg.address, &mut msg.account).into_account_info(); let bank_to_update: &mut BankWrapper = self.banks.get_mut(bank_to_update_pk).unwrap(); - bank_to_update.oracle_adapter.price_adapter = - OraclePriceFeedAdapter::try_from_bank_config_with_max_age( - &bank_to_update.bank.config, - &[oracle_ai.clone()], - &Clock::default(), - i64::MAX as u64, - ) - .unwrap(); + + let oracle_price_adapter = match bank_to_update.bank.config.oracle_setup + { + OracleSetup::SwitchboardPull => { + let mut offsets_data = + [0u8; std::mem::size_of::()]; + offsets_data.copy_from_slice( + &msg.account.data + [8..std::mem::size_of::() + 8], + ); + let swb_feed = crate::utils::load_swb_pull_account_from_bytes( + &offsets_data, + ) + .unwrap(); + + let feed_hash = hex::encode(swb_feed.feed_hash); + bank_to_update.oracle_adapter.swb_feed_hash = Some(feed_hash); + + OraclePriceFeedAdapter::SwitchboardPull( + SwitchboardPullPriceFeed { + feed: Box::new((&swb_feed).into()), + }, + ) + } + _ => { + let oracle_account_info = + (&msg.address, &mut msg.account).into_account_info(); + OraclePriceFeedAdapter::try_from_bank_config_with_max_age( + &bank_to_update.bank.config, + &[oracle_account_info], + &Clock::default(), + i64::MAX as u64, + ) + .unwrap() + } + }; + + bank_to_update.oracle_adapter.price_adapter = oracle_price_adapter; } } AccountType::MarginfiAccount => { @@ -181,7 +217,7 @@ impl Liquidator { { break; } - if let Ok(mut accounts) = self.process_all_accounts() { + if let Ok(mut accounts) = self.process_all_accounts().await { // Accounts are sorted from the highest profit to the lowest accounts.sort_by(|a, b| a.profit.cmp(&b.profit)); accounts.reverse(); @@ -203,28 +239,6 @@ impl Liquidator { ); } } - /*if let Some(highest_profit_account) = accounts.first() { - info!( - "Liquidating account {:?}", - highest_profit_account.liquidate_account.address - ); - if let Err(e) = self - .liquidator_account - .liquidate( - &highest_profit_account.liquidate_account, - &highest_profit_account.asset_bank, - &highest_profit_account.liab_bank, - highest_profit_account.asset_amount, - &highest_profit_account.banks, - ) - .await - { - info!( - "Failed to liquidate account {:?}, error: {:?}", - highest_profit_account.liquidate_account.address, e - ); - } - }*/ } break; } @@ -234,7 +248,27 @@ impl Liquidator { /// Starts processing/evaluate all account, checking /// if a liquidation is necessary/needed - fn process_all_accounts(&self) -> anyhow::Result> { + async fn process_all_accounts(&mut self) -> anyhow::Result> { + // Update switchboard pull prices with crossbar + let swb_feed_hashes = self + .banks + .values() + .filter_map(|bank| { + if let Some(feed_hash) = &bank.oracle_adapter.swb_feed_hash { + Some((bank.address, feed_hash.clone())) + } else { + None + } + }) + .collect::>(); + + let simulated_prices = self.crossbar_client.simulate(swb_feed_hashes).await; + + for (bank_pk, price) in simulated_prices { + let bank = self.banks.get_mut(&bank_pk).unwrap(); + bank.oracle_adapter.simulated_price = Some(price); + } + let accounts = self .marginfi_accounts .par_iter() @@ -243,6 +277,25 @@ impl Liquidator { return None; } + let (deposit_shares, liabs_shares) = account.get_deposits_and_liabilities_shares(); + + let deposit_values = self + .get_value_of_shares( + deposit_shares, + &BalanceSide::Assets, + RequirementType::Maintenance, + ) + .unwrap(); + + if deposit_values + .iter() + .map(|(v, _)| v.to_num::()) + .sum::() + < BANKRUPT_THRESHOLD + { + return None; + } + let (asset_bank_pk, liab_bank_pk) = match self.find_liquidation_bank_candidates(account) { Ok(Some((asset_bank_pk, liab_bank_pk))) => (asset_bank_pk, liab_bank_pk), @@ -318,12 +371,10 @@ impl Liquidator { let lower_price = bank .oracle_adapter - .price_adapter .get_price_of_type(OraclePriceType::TimeWeighted, Some(PriceBias::Low))?; let higher_price = bank .oracle_adapter - .price_adapter .get_price_of_type(OraclePriceType::TimeWeighted, Some(PriceBias::High))?; let token_decimals = bank.bank.mint_decimals as usize; @@ -460,9 +511,11 @@ impl Liquidator { RequirementType::Maintenance, )?; - debug!("Account {:?}", account.address); - debug!("Health {:?}", maintenance_health); - debug!("Liquidator profit {:?}", liquidator_profit); + if liquidator_profit > self.config.min_profit { + debug!("Account {:?}", account.address); + debug!("Health {:?}", maintenance_health); + debug!("Liquidator profit {:?}", liquidator_profit); + } Ok((max_liquidatable_asset_amount, liquidator_profit)) } @@ -671,23 +724,38 @@ impl Liquidator { (oracle_address.unwrap(), oracle_account.unwrap()) }; - let oracle_account_info = (&oracle_address, &mut oracle_account).into_account_info(); + let price_adapter = match bank.config.oracle_setup { + OracleSetup::SwitchboardPull => { + let mut offsets_data = [0u8; std::mem::size_of::()]; + offsets_data.copy_from_slice( + &oracle_account.data[8..std::mem::size_of::() + 8], + ); + let swb_feed = + crate::utils::load_swb_pull_account_from_bytes(&offsets_data).unwrap(); + + OraclePriceFeedAdapter::SwitchboardPull(SwitchboardPullPriceFeed { + feed: Box::new((&swb_feed).into()), + }) + } + _ => { + let oracle_account_info = + (&oracle_address, &mut oracle_account).into_account_info(); + OraclePriceFeedAdapter::try_from_bank_config_with_max_age( + &bank.config, + &[oracle_account_info], + &Clock::default(), + i64::MAX as u64, + ) + .unwrap() + } + }; self.banks.insert( *bank_address, BankWrapper::new( *bank_address, *bank, - OracleWrapper::new( - oracle_address, - OraclePriceFeedAdapter::try_from_bank_config_with_max_age( - &bank.config, - &[oracle_account_info], - &Clock::default(), - i64::MAX as u64, - ) - .unwrap(), - ), + OracleWrapper::new(oracle_address, price_adapter), ), ); @@ -733,6 +801,13 @@ impl Liquidator { continue; } + if !matches!( + bank.bank.config.operational_state, + BankOperationalState::Operational + ) { + continue; + } + let value = match balance_side { BalanceSide::Liabilities => bank .calc_value(share.0, BalanceSide::Liabilities, requirement_type) diff --git a/src/main.rs b/src/main.rs index 1ee24c0..a0c70bd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -35,6 +35,9 @@ mod config; /// Transactio manager mod transaction_manager; +/// Crossbar client +mod crossbar; + #[tokio::main] async fn main() -> Result<(), Box> { // Assemble logger, with INFO as default log level diff --git a/src/rebalancer.rs b/src/rebalancer.rs index 31ba80d..d18c628 100644 --- a/src/rebalancer.rs +++ b/src/rebalancer.rs @@ -1,9 +1,10 @@ use crate::{ config::{GeneralConfig, RebalancerCfg}, + crossbar::CrossbarMaintainer, geyser::{AccountType, GeyserUpdate}, sender::{SenderCfg, TransactionSender}, token_account_manager::TokenAccountManager, - transaction_manager::BatchTransactions, + transaction_manager::{BatchTransactions, RawTransaction}, utils::{ accessor, batch_get_multiple_accounts, calc_weighted_assets_new, calc_weighted_liabs_new, BankAccountWithPriceFeedEva, @@ -28,10 +29,12 @@ use marginfi::{ constants::EXP_10_I80F48, state::{ marginfi_account::{BalanceSide, MarginfiAccount, RequirementType}, - price::{OraclePriceFeedAdapter, PriceAdapter, PriceBias}, + price::{OraclePriceFeedAdapter, OracleSetup, PriceBias, SwitchboardPullPriceFeed}, }, }; -use solana_client::rpc_client::RpcClient; +use solana_client::{ + nonblocking::rpc_client::RpcClient as NonBlockingRpcClient, rpc_client::RpcClient, +}; use solana_program::pubkey::Pubkey; use solana_sdk::{ account_info::IntoAccountInfo, clock::Clock, commitment_config::CommitmentConfig, @@ -40,9 +43,12 @@ use solana_sdk::{ use std::{ cmp::min, collections::{HashMap, HashSet}, + str::FromStr, sync::{atomic::AtomicBool, Arc}, }; - +use switchboard_on_demand::PullFeedAccountData; +use switchboard_on_demand_client::QueueAccountData; +use switchboard_on_demand_client::{FetchUpdateManyParams, Gateway, PullFeed}; /// The rebalancer is responsible to keep the liquidator account /// "rebalanced" -> Document this better pub struct Rebalancer { @@ -59,6 +65,7 @@ pub struct Rebalancer { swap_mint_bank_pk: Option, geyser_receiver: Receiver, stop_liquidations: Arc, + crossbar_client: CrossbarMaintainer, } impl Rebalancer { @@ -78,8 +85,7 @@ impl Rebalancer { transaction_tx.clone(), general_config.clone(), ) - .await - .unwrap(); + .await?; let preferred_mints = config.preferred_mints.iter().cloned().collect(); @@ -97,6 +103,7 @@ impl Rebalancer { swap_mint_bank_pk: None, geyser_receiver, stop_liquidations: stop_liquidation, + crossbar_client: CrossbarMaintainer::new(), }) } @@ -178,24 +185,52 @@ impl Rebalancer { pub async fn start(&mut self) -> anyhow::Result<()> { let max_duration = std::time::Duration::from_secs(10); loop { - let start = std::time::Instant::now(); + let start = std::time::Instant::now().checked_sub(max_duration).unwrap(); while let Ok(mut msg) = self.geyser_receiver.recv() { debug!("Received message {:?}", msg); match msg.account_type { AccountType::OracleAccount => { if let Some(bank_to_update_pk) = self.oracle_to_bank.get(&msg.address) { - let oracle_ai = (&msg.address, &mut msg.account).into_account_info(); let bank_to_update: &mut BankWrapper = self.banks.get_mut(bank_to_update_pk).unwrap(); - bank_to_update.oracle_adapter.price_adapter = - OraclePriceFeedAdapter::try_from_bank_config_with_max_age( - &bank_to_update.bank.config, - &[oracle_ai.clone()], - &Clock::default(), - i64::MAX as u64, - ) - .unwrap(); + let oracle_price_adapter = match bank_to_update.bank.config.oracle_setup + { + OracleSetup::SwitchboardPull => { + let mut offsets_data = + [0u8; std::mem::size_of::()]; + offsets_data.copy_from_slice( + &msg.account.data + [8..std::mem::size_of::() + 8], + ); + let swb_feed = crate::utils::load_swb_pull_account_from_bytes( + &offsets_data, + ) + .unwrap(); + + let feed_hash = hex::encode(swb_feed.feed_hash); + bank_to_update.oracle_adapter.swb_feed_hash = Some(feed_hash); + + OraclePriceFeedAdapter::SwitchboardPull( + SwitchboardPullPriceFeed { + feed: Box::new((&swb_feed).into()), + }, + ) + } + _ => { + let oracle_account_info = + (&msg.address, &mut msg.account).into_account_info(); + OraclePriceFeedAdapter::try_from_bank_config_with_max_age( + &bank_to_update.bank.config, + &[oracle_account_info], + &Clock::default(), + i64::MAX as u64, + ) + .unwrap() + } + }; + + bank_to_update.oracle_adapter.price_adapter = oracle_price_adapter; } } AccountType::MarginfiAccount => { @@ -226,14 +261,69 @@ impl Rebalancer { } } - async fn needs_to_be_relanced(&self) -> bool { - self.should_stop_liquidations().await; + async fn needs_to_be_relanced(&mut self) -> bool { + // Update switchboard pull prices with crossbar + let swb_feed_hashes = self + .banks + .values() + .filter_map(|bank| { + if let Some(feed_hash) = &bank.oracle_adapter.swb_feed_hash { + Some((bank.address, feed_hash.clone())) + } else { + None + } + }) + .collect::>(); + + let simulated_prices = self.crossbar_client.simulate(swb_feed_hashes).await; + + for (bank_pk, price) in simulated_prices { + let bank = self.banks.get_mut(&bank_pk).unwrap(); + bank.oracle_adapter.simulated_price = Some(price); + } + + self.should_stop_liquidations().await.unwrap(); + self.has_tokens_in_token_accounts() || self.has_non_preferred_deposits() || self.has_liabilities() } async fn rebalance_accounts(&mut self) -> anyhow::Result<()> { + let active_banks = self.liquidator_account.account_wrapper.get_active_banks(); + + let active_swb_oracles: Vec = active_banks + .iter() + .filter_map(|&bank_pk| { + self.banks.get(&bank_pk).and_then(|bank| { + if bank.oracle_adapter.is_switchboard_pull() { + Some(bank.oracle_adapter.address) + } else { + None + } + }) + }) + .collect(); + + if !active_swb_oracles.is_empty() { + if let Ok((ix, lut)) = PullFeed::fetch_update_many_ix( + &self.liquidator_account.non_blocking_rpc_client, + FetchUpdateManyParams { + feeds: active_swb_oracles, + payer: self.general_config.signer_pubkey, + gateway: self.liquidator_account.swb_gateway.clone(), + num_signatures: Some(1), + ..Default::default() + }, + ) + .await + { + self.liquidator_account + .transaction_tx + .send(vec![RawTransaction::new(vec![ix]).with_lookup_tables(lut)]) + .unwrap(); + } + } debug!("Rebalancing accounts"); self.sell_non_preferred_deposits().await?; self.repay_liabilities().await?; @@ -739,7 +829,7 @@ impl Rebalancer { ) -> anyhow::Result { let bank = self.banks.get(bank_pk).unwrap(); - let price = bank.oracle_adapter.price_adapter.get_price_of_type( + let price = bank.oracle_adapter.get_price_of_type( marginfi::state::price::OraclePriceType::RealTime, price_bias, )?; diff --git a/src/sender.rs b/src/sender.rs index 898f31a..b12864a 100644 --- a/src/sender.rs +++ b/src/sender.rs @@ -2,7 +2,7 @@ use crate::wrappers::marginfi_account::TxConfig; use log::{error, info}; use serde::Deserialize; use solana_client::rpc_client::{RpcClient, SerializableTransaction}; -use solana_client::rpc_config::RpcSimulateTransactionConfig; +use solana_client::rpc_config::{RpcSendTransactionConfig, RpcSimulateTransactionConfig}; use solana_sdk::signature::Signature; use solana_sdk::{ commitment_config::CommitmentConfig, @@ -165,7 +165,13 @@ impl TransactionSender { } (0..cfg.spam_times).try_for_each(|_| { - rpc.send_transaction(transaction)?; + rpc.send_transaction_with_config( + transaction, + RpcSendTransactionConfig { + skip_preflight: true, + ..Default::default() + }, + )?; Ok::<_, Box>(()) })?; diff --git a/src/token_account_manager.rs b/src/token_account_manager.rs index 1956796..816a93f 100644 --- a/src/token_account_manager.rs +++ b/src/token_account_manager.rs @@ -3,27 +3,20 @@ use std::{ sync::{Arc, RwLock}, }; -use anchor_lang::accounts::program; use anchor_spl::associated_token; -use crossbeam::epoch::Owned; use log::{debug, error, info}; use rayon::iter::{IndexedParallelIterator, IntoParallelRefIterator, ParallelIterator}; use sha2::{Digest, Sha256}; use solana_client::rpc_client::RpcClient; -use solana_sdk::{ - account::Account, - pubkey::Pubkey, - signature::Keypair, - signer::{SeedDerivable, Signer}, -}; +use solana_sdk::{account::Account, pubkey::Pubkey, signature::Keypair, signer::Signer}; use crate::{ sender::{SenderCfg, TransactionSender}, utils::{batch_get_multiple_accounts, BatchLoadingConfig}, }; -const TOKEN_ACCOUNT_SEED: &[u8] = b"liquidator_ta"; -const MAX_INIT_TA_IXS: usize = 4; +// const TOKEN_ACCOUNT_SEED: &[u8] = b"liquidator_ta"; +const MAX_INIT_TA_IXS: usize = 10; #[derive(Debug, Clone, thiserror::Error)] pub enum TokenAccountManagerError { @@ -230,15 +223,15 @@ fn get_liquidator_seed(signer: Pubkey, mint: Pubkey, seed: &[u8]) -> [u8; 32] { hasher.finalize().into() } -fn get_keypair_for_token_account( - signer: Pubkey, - mint: Pubkey, - seed: &[u8], -) -> Result { - let keypair_seed = get_liquidator_seed(signer, mint, seed); - Keypair::from_seed(&keypair_seed) - .map_err(|_| TokenAccountManagerError::SetupFailed("Keypair::from_seed failed")) -} +// fn get_keypair_for_token_account( +// signer: Pubkey, +// mint: Pubkey, +// seed: &[u8], +// ) -> Result { +// let keypair_seed = get_liquidator_seed(signer, mint, seed); +// Keypair::from_seed(&keypair_seed) +// .map_err(|_| TokenAccountManagerError::SetupFailed("Keypair::from_seed failed")) +// } fn get_address_for_token_account( signer: Pubkey, diff --git a/src/transaction_manager.rs b/src/transaction_manager.rs index 25a3d90..7f11a3e 100644 --- a/src/transaction_manager.rs +++ b/src/transaction_manager.rs @@ -5,7 +5,7 @@ use jito_protos::searcher::{ NextScheduledLeaderRequest, SubscribeBundleResultsRequest, }; use jito_searcher_client::{get_searcher_client_no_auth, send_bundle_with_confirmation}; -use log::error; +use log::{debug, error}; use solana_address_lookup_table_program::state::AddressLookupTable; use solana_client::{ nonblocking::rpc_client::RpcClient, rpc_client::RpcClient as NonBlockRpc, @@ -52,11 +52,30 @@ pub struct TransactionManager { lookup_tables: Vec, } -/// Type alias for a batch of transactions -/// A batch of transactions is a vector of vectors of instructions -/// Each vector of instructions represents a single transaction -/// The outer vector represents a batch of transactions -pub type BatchTransactions = Vec>; +// Type alias for a batch of transactions +// A batch of transactions is a vector of vectors of instructions +// Each vector of instructions represents a single transaction +// The outer vector represents a batch of transactions +pub type BatchTransactions = Vec; + +pub struct RawTransaction { + pub instructions: Vec, + pub lookup_tables: Option>, +} + +impl RawTransaction { + pub fn new(instructions: Vec) -> Self { + Self { + instructions, + lookup_tables: None, + } + } + + pub fn with_lookup_tables(mut self, lookup_tables: Vec) -> Self { + self.lookup_tables = Some(lookup_tables); + self + } +} impl TransactionManager { /// Creates a new transaction manager @@ -109,6 +128,7 @@ impl TransactionManager { continue; } }; + debug!("Waiting for Jito leader..."); loop { let next_leader = match self .searcher_client @@ -125,30 +145,29 @@ impl TransactionManager { let num_slots = next_leader.next_leader_slot - next_leader.current_slot; if num_slots <= LEADERSHIP_THRESHOLD { + debug!("Sending bundle"); break; } tokio::time::sleep(SLEEP_DURATION).await; } - for tx in transactions { - let transaction = Self::send_transaction( - tx.clone(), - self.searcher_client.clone(), - self.rpc.clone(), - ); - tokio::spawn(async move { - if let Err(e) = transaction.await { - error!("Failed to send transaction: {:?}", e); - } - }); - } + let transaction = Self::send_transactions( + transactions, + self.searcher_client.clone(), + self.rpc.clone(), + ); + tokio::spawn(async move { + if let Err(e) = transaction.await { + error!("Failed to send transaction: {:?}", e); + } + }); } } /// Sends a transaction/bundle of transactions to the jito /// block engine and waits for confirmation - async fn send_transaction( - transaction: VersionedTransaction, + async fn send_transactions( + transactions: Vec, mut searcher_client: SearcherServiceClient, rpc: Arc, ) -> anyhow::Result<()> { @@ -158,7 +177,7 @@ impl TransactionManager { .into_inner(); if let Err(e) = send_bundle_with_confirmation( - &[transaction], + &transactions, &rpc, &mut searcher_client, &mut bundle_results_subscription, @@ -229,8 +248,9 @@ impl TransactionManager { let blockhash = self.rpc.get_latest_blockhash().await?; let mut txs = Vec::new(); - for mut ixs in instructions { - ixs.push(ComputeBudgetInstruction::set_compute_unit_limit(500_000)); + for mut raw_transaction in instructions { + let mut ixs = raw_transaction.instructions; + ixs.push(ComputeBudgetInstruction::set_compute_unit_limit(1_000_000)); ixs.push(transfer( &self.keypair.pubkey(), &self.tip_accounts[0], @@ -240,7 +260,11 @@ impl TransactionManager { VersionedMessage::V0(v0::Message::try_compile( &self.keypair.pubkey(), &ixs, - &self.lookup_tables, + if raw_transaction.lookup_tables.is_some() { + raw_transaction.lookup_tables.as_ref().unwrap() + } else { + &self.lookup_tables + }, blockhash, )?), &[&self.keypair], diff --git a/src/utils.rs b/src/utils.rs index 15e4dc1..3b12e61 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,9 +1,3 @@ -use std::{ - collections::HashMap, - str::FromStr, - sync::{atomic::AtomicUsize, Arc, RwLock}, -}; - use anyhow::{anyhow, Result}; use backoff::ExponentialBackoff; use fixed::types::I80F48; @@ -14,7 +8,7 @@ use marginfi::{ state::{ marginfi_account::{calc_value, Balance, BalanceSide, LendingAccount, RequirementType}, marginfi_group::{Bank, BankConfig, BankVaultType, RiskTier}, - price::{PriceAdapter, PriceBias, PythPushOraclePriceFeed}, + price::{PriceBias, PythPushOraclePriceFeed}, }, }; use rayon::{iter::ParallelIterator, slice::ParallelSlice}; @@ -22,7 +16,20 @@ use serde::{ser::SerializeSeq, Deserialize, Deserializer, Serializer}; use solana_account_decoder::UiAccountEncoding; use solana_client::rpc_config::RpcAccountInfoConfig; use solana_program::pubkey::Pubkey; -use solana_sdk::account::Account; +use solana_sdk::{ + account::Account, + account_info::AccountInfo, + signature::{read_keypair_file, Keypair}, +}; +use std::{ + collections::HashMap, + io::Write, + path::PathBuf, + str::FromStr, + sync::{atomic::AtomicUsize, Arc, RwLock}, +}; +use switchboard_on_demand::PullFeedAccountData; +use url::Url; use yellowstone_grpc_proto::geyser::SubscribeUpdateAccountInfo; use crate::wrappers::bank::BankWrapper; @@ -289,7 +296,7 @@ impl<'a> BankAccountWithPriceFeedEva<'a> { .map(move |balance| { let bank = banks .get(&balance.bank_pk) - .ok_or_else(|| anyhow::anyhow!("Bank not found"))? + .ok_or_else(|| anyhow::anyhow!("Bank {:?} not found", balance.bank_pk))? .clone(); Ok(BankAccountWithPriceFeedEva { bank, balance }) @@ -333,12 +340,12 @@ impl<'a> BankAccountWithPriceFeedEva<'a> { ) -> anyhow::Result { match bank.config.risk_tier { RiskTier::Collateral => { - let price_feed = &self.bank.oracle_adapter.price_adapter; + let oracle_adapter = &self.bank.oracle_adapter; let mut asset_weight = bank .config .get_weight(requirement_type, BalanceSide::Assets); - let lower_price = price_feed.get_price_of_type( + let lower_price = oracle_adapter.get_price_of_type( requirement_type.get_oracle_price_type(), Some(PriceBias::Low), )?; @@ -370,16 +377,18 @@ impl<'a> BankAccountWithPriceFeedEva<'a> { requirement_type: RequirementType, bank: &Bank, ) -> MarginfiResult { - let price_feed = &self.bank.oracle_adapter.price_adapter; + let oracle_adapter = &self.bank.oracle_adapter; let liability_weight = bank .config .get_weight(requirement_type, BalanceSide::Liabilities); - let higher_price = price_feed.get_price_of_type( - requirement_type.get_oracle_price_type(), - Some(PriceBias::High), - )?; + let higher_price = oracle_adapter + .get_price_of_type( + requirement_type.get_oracle_price_type(), + Some(PriceBias::High), + ) + .unwrap(); calc_value( bank.get_liability_amount(self.balance.liability_shares.into())?, @@ -411,7 +420,7 @@ pub fn calc_weighted_assets_new( amount: I80F48, requirement_type: RequirementType, ) -> anyhow::Result { - let price_feed = &bank.oracle_adapter.price_adapter; + let oracle_adapter = &bank.oracle_adapter; let mut asset_weight = bank .bank .config @@ -424,7 +433,7 @@ pub fn calc_weighted_assets_new( }; let lower_price = - price_feed.get_price_of_type(requirement_type.get_oracle_price_type(), price_bias)?; + oracle_adapter.get_price_of_type(requirement_type.get_oracle_price_type(), price_bias)?; if matches!(requirement_type, RequirementType::Initial) { if let Some(discount) = bank @@ -451,7 +460,7 @@ pub fn calc_weighted_assets( requirement_type: RequirementType, ) -> anyhow::Result { let bank_wrapper_ref = bank_rw_lock.read().unwrap(); - let price_feed = &bank_wrapper_ref.oracle_adapter.price_adapter; + let oracle_adapter = &bank_wrapper_ref.oracle_adapter; let mut asset_weight = bank_wrapper_ref .bank .config @@ -464,7 +473,7 @@ pub fn calc_weighted_assets( }; let lower_price = - price_feed.get_price_of_type(requirement_type.get_oracle_price_type(), price_bias)?; + oracle_adapter.get_price_of_type(requirement_type.get_oracle_price_type(), price_bias)?; if matches!(requirement_type, RequirementType::Initial) { if let Some(discount) = bank_wrapper_ref @@ -504,7 +513,6 @@ pub fn calc_weighted_liabs_new( let higher_price = bank .oracle_adapter - .price_adapter .get_price_of_type(requirement_type.get_oracle_price_type(), price_bias)?; Ok(calc_value( @@ -523,7 +531,7 @@ pub fn calc_weighted_liabs( ) -> anyhow::Result { let bank_wrapper_ref = bank_rw_lock.read().unwrap(); let bank = &bank_wrapper_ref.bank; - let price_feed = &bank_wrapper_ref.oracle_adapter.price_adapter; + let oracle_adapter = &bank_wrapper_ref.oracle_adapter; let liability_weight = bank .config .get_weight(requirement_type, BalanceSide::Liabilities); @@ -535,7 +543,7 @@ pub fn calc_weighted_liabs( }; let higher_price = - price_feed.get_price_of_type(requirement_type.get_oracle_price_type(), price_bias)?; + oracle_adapter.get_price_of_type(requirement_type.get_oracle_price_type(), price_bias)?; Ok(calc_value( amount, @@ -565,3 +573,69 @@ pub fn find_oracle_keys(bank_config: &BankConfig) -> Vec { _ => vec![bank_config.oracle_keys.first().unwrap().clone()], } } + +pub fn load_swb_pull_account(account_info: &AccountInfo) -> anyhow::Result { + let bytes = &account_info.data.borrow().to_vec()[8..std::mem::size_of::()]; + + Ok(load_swb_pull_account_from_bytes(bytes)?) +} + +pub fn load_swb_pull_account_from_bytes(bytes: &[u8]) -> anyhow::Result { + if bytes + .as_ptr() + .align_offset(std::mem::align_of::()) + != 0 + { + return Err(anyhow::anyhow!("Invalid alignment")); + } + + let num = bytes.len() / std::mem::size_of::(); + let mut vec: Vec = Vec::with_capacity(num); + + unsafe { + vec.set_len(num); + std::ptr::copy_nonoverlapping( + bytes[..std::mem::size_of::()].as_ptr(), + vec.as_mut_ptr() as *mut u8, + bytes.len(), + ); + } + + Ok(vec[0]) +} + +pub fn expand_tilde(path: &str) -> PathBuf { + if path.starts_with("~") { + if let Some(home) = dirs::home_dir() { + return home.join(&path[2..]); + } + } + PathBuf::from(path) +} + +pub fn is_valid_url(input: &str) -> bool { + Url::parse(input).is_ok() +} + +pub fn prompt_user(prompt_text: &str) -> anyhow::Result { + print!("{}", prompt_text); + let mut input = String::new(); + std::io::stdout().flush()?; + std::io::stdin().read_line(&mut input)?; + input.pop(); + Ok(input) +} + +/// Simply asks the keypair path until it is a valid one, +/// Returns (keypair_path, signer_keypair) +pub fn ask_keypair_until_valid() -> anyhow::Result<(PathBuf, Keypair)> { + loop { + let keypair_path = expand_tilde(&prompt_user("Keypair file path [required]: ")?); + match read_keypair_file(&keypair_path) { + Ok(keypair) => return Ok((keypair_path, keypair)), + Err(_) => { + println!("Failed to load the keypair from the provided path. Please try again"); + } + } + } +} diff --git a/src/wrappers/bank.rs b/src/wrappers/bank.rs index f9c0d2f..edbeb15 100644 --- a/src/wrappers/bank.rs +++ b/src/wrappers/bank.rs @@ -63,7 +63,6 @@ impl BankWrapper { let price = self .oracle_adapter - .price_adapter .get_price_of_type(oracle_type, price_bias) .unwrap(); @@ -80,7 +79,6 @@ impl BankWrapper { let price = self .oracle_adapter - .price_adapter .get_price_of_type(oracle_type, price_bias) .unwrap(); @@ -97,7 +95,6 @@ impl BankWrapper { let price = self .oracle_adapter - .price_adapter .get_price_of_type(oracle_type, price_bias) .unwrap(); diff --git a/src/wrappers/liquidator_account.rs b/src/wrappers/liquidator_account.rs index 335fd3a..bd0e649 100644 --- a/src/wrappers/liquidator_account.rs +++ b/src/wrappers/liquidator_account.rs @@ -2,17 +2,20 @@ use super::{bank::BankWrapper, marginfi_account::MarginfiAccountWrapper}; use crate::{ config::GeneralConfig, marginfi_ixs::{make_deposit_ix, make_liquidate_ix, make_repay_ix, make_withdraw_ix}, - transaction_manager::BatchTransactions, + transaction_manager::{BatchTransactions, RawTransaction}, }; use crossbeam::channel::Sender; use marginfi::state::{marginfi_account::MarginfiAccount, marginfi_group::BankVaultType}; -use solana_client::rpc_client::RpcClient; +use solana_client::{ + nonblocking::rpc_client::RpcClient as NonBlockingRpcClient, rpc_client::RpcClient, +}; use solana_program::pubkey::Pubkey; use solana_sdk::{ signature::{read_keypair_file, Keypair}, signer::Signer, }; -use std::{collections::HashMap, sync::Arc}; +use std::{collections::HashMap, str::FromStr, sync::Arc}; +use switchboard_on_demand_client::{FetchUpdateManyParams, Gateway, PullFeed, QueueAccountData}; /// Wraps the liquidator account into a dedicated strecture pub struct LiquidatorAccount { @@ -21,7 +24,9 @@ pub struct LiquidatorAccount { program_id: Pubkey, token_program_per_mint: HashMap, group: Pubkey, - transaction_tx: Sender, + pub transaction_tx: Sender, + pub swb_gateway: Gateway, + pub non_blocking_rpc_client: NonBlockingRpcClient, } impl LiquidatorAccount { @@ -36,17 +41,31 @@ impl LiquidatorAccount { let account = rpc_client.get_account(&liquidator_pubkey)?; let marginfi_account = bytemuck::from_bytes::(&account.data[8..]); let account_wrapper = MarginfiAccountWrapper::new(liquidator_pubkey, *marginfi_account); - - let program_id = marginfi::id(); let group = account_wrapper.account.group; + let non_blocking_rpc_client = NonBlockingRpcClient::new(config.rpc_url.clone()); + + let queue = QueueAccountData::load( + &non_blocking_rpc_client, + &Pubkey::from_str("A43DyUGA7s8eXPxqEjJY6EBu1KKbNgfxF8h17VAHn13w").unwrap(), + ) + .await + .unwrap(); + let swb_gateway = queue + .fetch_gateways(&non_blocking_rpc_client) + .await + .unwrap()[0] + .clone(); + Ok(Self { account_wrapper, signer_keypair, - program_id, + program_id: config.marginfi_program_id, group, transaction_tx, token_program_per_mint: HashMap::new(), + swb_gateway, + non_blocking_rpc_client, }) } @@ -99,6 +118,46 @@ impl LiquidatorAccount { let liquidatee_observation_accounts = liquidate_account.get_observation_accounts(&[], &[], banks); + let joined_observation_accounts = liquidator_observation_accounts + .iter() + .chain(liquidatee_observation_accounts.iter()) + .cloned() + .collect::>(); + + let observation_swb_oracles = joined_observation_accounts + .iter() + .filter_map(|&pk| { + banks.get(&pk).and_then(|bank| { + if bank.oracle_adapter.is_switchboard_pull() { + Some(bank.oracle_adapter.address) + } else { + None + } + }) + }) + .collect::>(); + + let crank_data = if !observation_swb_oracles.is_empty() { + if let Ok((ix, luts)) = PullFeed::fetch_update_many_ix( + &self.non_blocking_rpc_client, + FetchUpdateManyParams { + feeds: observation_swb_oracles, + payer: self.signer_keypair.pubkey(), + gateway: self.swb_gateway.clone(), + num_signatures: Some(1), + ..Default::default() + }, + ) + .await + { + Some((ix, luts)) + } else { + return Err(anyhow::anyhow!("Failed to fetch crank data")); + } + } else { + None + }; + let liquidate_ix = make_liquidate_ix( self.program_id, self.group, @@ -119,8 +178,13 @@ impl LiquidatorAccount { asset_amount, ); - // Double vec implies that is a single bundle - self.transaction_tx.send(vec![vec![liquidate_ix]])?; + let mut bundle = vec![]; + if let Some((crank_ix, crank_lut)) = crank_data { + bundle.push(RawTransaction::new(vec![crank_ix]).with_lookup_tables(crank_lut)); + } + bundle.push(RawTransaction::new(vec![liquidate_ix])); + + self.transaction_tx.send(bundle)?; Ok(()) } @@ -171,8 +235,8 @@ impl LiquidatorAccount { withdraw_all, ); - // Double vec implies that is a single bundle - self.transaction_tx.send(vec![vec![withdraw_ix]])?; + self.transaction_tx + .send(vec![RawTransaction::new(vec![withdraw_ix])])?; Ok(()) } @@ -205,8 +269,8 @@ impl LiquidatorAccount { repay_all, ); - // Double vec implies that is a single bundle - self.transaction_tx.send(vec![vec![repay_ix]])?; + self.transaction_tx + .send(vec![RawTransaction::new(vec![repay_ix])])?; Ok(()) } @@ -237,8 +301,8 @@ impl LiquidatorAccount { amount, ); - // Double vec implies that is a single bundle - self.transaction_tx.send(vec![vec![deposit_ix]])?; + self.transaction_tx + .send(vec![RawTransaction::new(vec![deposit_ix])])?; Ok(()) } diff --git a/src/wrappers/marginfi_account.rs b/src/wrappers/marginfi_account.rs index 7ae4871..47b5a49 100644 --- a/src/wrappers/marginfi_account.rs +++ b/src/wrappers/marginfi_account.rs @@ -130,6 +130,17 @@ impl MarginfiAccountWrapper { (deposits, liabilities) } + pub fn get_active_banks(&self) -> Vec { + self.account + .lending_account + .balances + .clone() + .iter() + .filter(|b| b.active) + .map(|b| b.bank_pk) + .collect::>() + } + pub fn get_observation_accounts( &self, banks_to_include: &[Pubkey], diff --git a/src/wrappers/oracle.rs b/src/wrappers/oracle.rs index c06e875..58f0d96 100644 --- a/src/wrappers/oracle.rs +++ b/src/wrappers/oracle.rs @@ -1,10 +1,17 @@ -use marginfi::state::price::OraclePriceFeedAdapter; +use std::sync::Arc; + +use fixed::types::I80F48; +use marginfi::state::price::{OraclePriceFeedAdapter, OraclePriceType, PriceAdapter, PriceBias}; use solana_program::pubkey::Pubkey; +use tokio::sync::Mutex; #[derive(Clone)] pub struct OracleWrapper { pub address: Pubkey, pub price_adapter: OraclePriceFeedAdapter, + // Simulated price are only for swb pull oracles + pub simulated_price: Option, + pub swb_feed_hash: Option, } impl OracleWrapper { @@ -12,6 +19,28 @@ impl OracleWrapper { Self { address, price_adapter, + simulated_price: None, + swb_feed_hash: None, + } + } + + pub fn get_price_of_type( + &self, + oracle_type: OraclePriceType, + price_bias: Option, + ) -> anyhow::Result { + match self.simulated_price { + Some(price) => Ok(I80F48::from_num(price)), + None => Ok(self + .price_adapter + .get_price_of_type(oracle_type, price_bias)?), } } + + pub fn is_switchboard_pull(&self) -> bool { + matches!( + self.price_adapter, + OraclePriceFeedAdapter::SwitchboardPull(_) + ) + } } diff --git a/src/wrappers/token_account.rs b/src/wrappers/token_account.rs index 534dcde..25dbd2d 100644 --- a/src/wrappers/token_account.rs +++ b/src/wrappers/token_account.rs @@ -24,7 +24,6 @@ impl TokenAccountWrapper { let price = bank .oracle_adapter - .price_adapter .get_price_of_type(marginfi::state::price::OraclePriceType::RealTime, None)?; Ok(ui_amount * price)