diff --git a/CHANGELOG.md b/CHANGELOG.md index 29a1ed3a..59d03b6e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Features +- program/sdk: swift integration ([#52](https://github.com/drift-labs/jit-proxy/pull/52)) + ### Fixes ### Breaking diff --git a/Cargo.lock b/Cargo.lock index 720c3ec2..be0bc088 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -450,6 +450,18 @@ dependencies = [ "typenum", ] +[[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.5.1" @@ -509,6 +521,16 @@ dependencies = [ "hashbrown 0.13.2", ] +[[package]] +name = "borsh" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5327f6c99920069d1fe374aa743be1af0031dea9f250852cdf1ae6a0861ee24" +dependencies = [ + "borsh-derive 1.5.2", + "cfg_aliases", +] + [[package]] name = "borsh-derive" version = "0.9.3" @@ -535,6 +557,19 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "borsh-derive" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10aedd8f1a81a8aafbfde924b0e3061cd6fedd6f6bbcfc6a76e6fd426d7bfe26" +dependencies = [ + "once_cell", + "proc-macro-crate 3.2.0", + "proc-macro2", + "quote", + "syn 2.0.58", +] + [[package]] name = "borsh-derive-internal" version = "0.9.3" @@ -610,6 +645,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.20.0" @@ -636,6 +693,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" +[[package]] +name = "bytes" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" + [[package]] name = "cc" version = "1.2.1" @@ -653,6 +716,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + [[package]] name = "chrono" version = "0.4.38" @@ -697,6 +766,12 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + [[package]] name = "cpufeatures" version = "0.2.15" @@ -832,6 +907,19 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "derive_more" +version = "0.99.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f33878137e4dafd7fa914ad4e259e18a4e8e532b9617a2d0150262bf53abfce" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.58", +] + [[package]] name = "digest" version = "0.9.0" @@ -854,8 +942,8 @@ dependencies = [ [[package]] name = "drift" -version = "2.103.0" -source = "git+https://github.com/drift-labs/protocol-v2.git?rev=v2.103.0#63c72dad6e1aa7c9b121560a516b434b091c3295" +version = "2.106.0" +source = "git+https://github.com/drift-labs/protocol-v2.git?rev=v2.106.0#02df3b90a49d3f18695722e72ca465ff7d694238" dependencies = [ "ahash 0.8.6", "anchor-lang", @@ -874,6 +962,7 @@ dependencies = [ "openbook-v2-light", "phoenix-v1", "pyth-client", + "pyth-lazer-solana-contract", "pyth-solana-receiver-sdk", "pythnet-sdk", "serum_dex", @@ -1020,6 +1109,12 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + [[package]] name = "generic-array" version = "0.14.7" @@ -1066,6 +1161,15 @@ dependencies = [ "ahash 0.7.8", ] +[[package]] +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" version = "0.13.2" @@ -1200,6 +1304,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.12" @@ -1215,6 +1328,7 @@ dependencies = [ "anchor-spl", "bytemuck", "drift", + "solana-program", "static_assertions", ] @@ -1542,7 +1656,7 @@ checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" [[package]] name = "openbook-v2-light" version = "0.1.0" -source = "git+https://github.com/drift-labs/protocol-v2.git?rev=v2.103.0#63c72dad6e1aa7c9b121560a516b434b091c3295" +source = "git+https://github.com/drift-labs/protocol-v2.git?rev=v2.106.0#02df3b90a49d3f18695722e72ca465ff7d694238" dependencies = [ "anchor-lang", "borsh 0.10.3", @@ -1659,7 +1773,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" dependencies = [ "once_cell", - "toml_edit", + "toml_edit 0.19.15", +] + +[[package]] +name = "proc-macro-crate" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b" +dependencies = [ + "toml_edit 0.22.22", ] [[package]] @@ -1671,12 +1794,58 @@ dependencies = [ "unicode-ident", ] +[[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 = "pyth-client" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44de48029c54ec1ca570786b5baeb906b0fc2409c8e0145585e287ee7a526c72" +[[package]] +name = "pyth-lazer-protocol" +version = "0.1.2" +source = "git+https://github.com/drift-labs/pyth-crosschain?rev=57ed54b2e0b1ce74a6d99c95302fec8752d7108f#57ed54b2e0b1ce74a6d99c95302fec8752d7108f" +dependencies = [ + "anyhow", + "byteorder", + "derive_more", + "itertools 0.13.0", + "rust_decimal", + "serde", +] + +[[package]] +name = "pyth-lazer-solana-contract" +version = "0.2.0" +source = "git+https://github.com/drift-labs/pyth-crosschain?rev=57ed54b2e0b1ce74a6d99c95302fec8752d7108f#57ed54b2e0b1ce74a6d99c95302fec8752d7108f" +dependencies = [ + "anchor-lang", + "bytemuck", + "byteorder", + "pyth-lazer-protocol", + "solana-program", + "thiserror", +] + [[package]] name = "pyth-solana-receiver-sdk" version = "0.3.0" @@ -1727,6 +1896,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" @@ -1746,6 +1921,7 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ + "libc", "rand_chacha 0.3.1", "rand_core 0.6.4", ] @@ -1864,6 +2040,60 @@ 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 = "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.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b082d80e3e3cc52b2ed634388d436fe1f4de6af5786cc2de9ba9737527bdf555" +dependencies = [ + "arrayvec", + "borsh 1.5.2", + "bytes", + "num-traits", + "rand 0.8.5", + "rkyv", + "serde", + "serde_json", +] + [[package]] name = "rustc-hash" version = "1.1.0" @@ -1903,6 +2133,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "seahash" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" + [[package]] name = "semver" version = "1.0.23" @@ -2099,6 +2335,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 = "sized-chunks" version = "0.6.5" @@ -2564,7 +2806,7 @@ checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" [[package]] name = "switchboard" version = "0.1.0" -source = "git+https://github.com/drift-labs/protocol-v2.git?rev=v2.103.0#63c72dad6e1aa7c9b121560a516b434b091c3295" +source = "git+https://github.com/drift-labs/protocol-v2.git?rev=v2.106.0#02df3b90a49d3f18695722e72ca465ff7d694238" dependencies = [ "anchor-lang", ] @@ -2572,7 +2814,7 @@ dependencies = [ [[package]] name = "switchboard-on-demand" version = "0.1.0" -source = "git+https://github.com/drift-labs/protocol-v2.git?rev=v2.103.0#63c72dad6e1aa7c9b121560a516b434b091c3295" +source = "git+https://github.com/drift-labs/protocol-v2.git?rev=v2.106.0#02df3b90a49d3f18695722e72ca465ff7d694238" dependencies = [ "anchor-lang", "bytemuck", @@ -2601,6 +2843,12 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + [[package]] name = "termcolor" version = "1.4.1" @@ -2687,7 +2935,18 @@ checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ "indexmap", "toml_datetime", - "winnow", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.22.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" +dependencies = [ + "indexmap", + "toml_datetime", + "winnow 0.6.24", ] [[package]] @@ -2758,6 +3017,12 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "uuid" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "744018581f9a3454a9e15beb8a33b017183f1e7c0cd170232a2d1453b23a51c4" + [[package]] name = "version_check" version = "0.9.5" @@ -2954,6 +3219,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "winnow" +version = "0.6.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d71a593cc5c42ad7876e2c1fda56f314f3754c084128833e64f1345ff8a03a" +dependencies = [ + "memchr", +] + [[package]] name = "without-alloc" version = "0.2.2" @@ -2964,6 +3238,15 @@ dependencies = [ "unsize", ] +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + [[package]] name = "zerocopy" version = "0.7.35" diff --git a/programs/jit-proxy/Cargo.toml b/programs/jit-proxy/Cargo.toml index 49e63b6f..5a9a6692 100644 --- a/programs/jit-proxy/Cargo.toml +++ b/programs/jit-proxy/Cargo.toml @@ -17,6 +17,7 @@ default = [] anchor-lang = "0.29.0" anchor-spl = "0.29.0" bytemuck = { version = "1.4.0" } -drift = { git = "https://github.com/drift-labs/protocol-v2.git", rev = "v2.103.0", features = ["cpi", "mainnet-beta"]} +drift = { git = "https://github.com/drift-labs/protocol-v2.git", rev = "v2.106.0", features = ["cpi", "mainnet-beta"]} static_assertions = "1.1.0" +solana-program = "1.16" ahash = "=0.8.6" diff --git a/programs/jit-proxy/src/error.rs b/programs/jit-proxy/src/error.rs index 1155ec4f..08e6f186 100644 --- a/programs/jit-proxy/src/error.rs +++ b/programs/jit-proxy/src/error.rs @@ -23,4 +23,6 @@ pub enum ErrorCode { PositionLimitBreached, #[msg("NoFill")] NoFill, + #[msg("SwiftOrderDoesNotExist")] + SwiftOrderDoesNotExist, } diff --git a/programs/jit-proxy/src/instructions/jit.rs b/programs/jit-proxy/src/instructions/jit.rs index 7917f0bd..2000b779 100644 --- a/programs/jit-proxy/src/instructions/jit.rs +++ b/programs/jit-proxy/src/instructions/jit.rs @@ -1,19 +1,25 @@ +use anchor_lang::prelude::Pubkey; use anchor_lang::prelude::*; use drift::controller::position::PositionDirection; -use drift::cpi::accounts::PlaceAndMake; +use drift::cpi::accounts::{PlaceAndMake, PlaceAndMakeSwift}; use drift::error::DriftResult; use drift::instructions::optional_accounts::{load_maps, AccountMaps}; use drift::math::casting::Cast; use drift::math::safe_math::SafeMath; use drift::program::Drift; use drift::state::order_params::OrderParams; +use drift::state::perp_market_map::PerpMarketMap; +use drift::state::spot_market_map::SpotMarketMap; use drift::state::state::State; +use drift::state::swift_user::SwiftUserOrdersLoader; +use drift::state::user::Order; use drift::state::user::{MarketType as DriftMarketType, OrderTriggerCondition, OrderType}; use drift::state::user::{User, UserStats}; use std::collections::BTreeSet; use crate::error::ErrorCode; -use crate::state::{PostOnlyParam, PriceType}; +use crate::state::PriceType; +use drift::state::order_params::PostOnlyParam; pub fn jit<'c: 'info, 'info>( ctx: Context<'_, '_, 'c, 'info, Jit<'info>>, @@ -25,11 +31,189 @@ pub fn jit<'c: 'info, 'info>( let taker = ctx.accounts.taker.load()?; let maker = ctx.accounts.user.load()?; + let remaining_accounts_iter = &mut ctx.remaining_accounts.iter().peekable(); + let AccountMaps { + perp_market_map, + spot_market_map, + mut oracle_map, + } = load_maps( + remaining_accounts_iter, + &BTreeSet::new(), + &BTreeSet::new(), + slot, + None, + )?; + let taker_order = taker .get_order(params.taker_order_id) .ok_or(ErrorCode::TakerOrderNotFound)?; let market_type = taker_order.market_type; let market_index = taker_order.market_index; + + let oracle_price = if taker_order.market_type == DriftMarketType::Perp { + let perp_market = perp_market_map.get_ref(&taker_order.market_index)?; + oracle_map.get_price_data(&perp_market.oracle_id())?.price + } else { + let spot_market = spot_market_map.get_ref(&taker_order.market_index)?; + oracle_map.get_price_data(&spot_market.oracle_id())?.price + }; + + let (order_params, taker_base_asset_amount_unfilled, taker_price, maker_price) = process_order( + &maker, + &perp_market_map, + &spot_market_map, + taker_order, + slot, + params.max_position, + params.min_position, + oracle_price, + params.get_worst_price(oracle_price, taker_order.direction)?, + params.post_only.unwrap_or(PostOnlyParam::MustPostOnly), + )?; + + drop(taker); + drop(maker); + + place_and_make(&ctx, params.taker_order_id, order_params)?; + + let taker = ctx.accounts.taker.load()?; + + let taker_base_asset_amount_unfilled_after = match taker.get_order(params.taker_order_id) { + Some(order) => order.get_base_asset_amount_unfilled(None)?, + None => 0, + }; + + if taker_base_asset_amount_unfilled_after == taker_base_asset_amount_unfilled { + // taker order failed to fill + msg!( + "taker price = {} maker price = {} oracle price = {}", + taker_price, + maker_price, + oracle_price + ); + msg!("jit params {:?}", params); + if market_type == DriftMarketType::Perp { + let perp_market = perp_market_map.get_ref(&market_index)?; + let reserve_price = perp_market.amm.reserve_price()?; + let (bid_price, ask_price) = perp_market.amm.bid_ask_price(reserve_price)?; + msg!( + "vamm bid price = {} vamm ask price = {}", + bid_price, + ask_price + ); + } + return Err(ErrorCode::NoFill.into()); + } + + Ok(()) +} + +pub fn jit_swift<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, JitSwift<'info>>, + params: JitSwiftParams, +) -> Result<()> { + let clock = Clock::get()?; + let slot = clock.slot; + + let taker = ctx.accounts.taker.load()?; + let maker = ctx.accounts.user.load()?; + + let taker_swift_account = ctx.accounts.taker_swift_user_orders.load()?; + let taker_order_id = taker_swift_account + .iter() + .find(|swift_order_id| swift_order_id.uuid == params.swift_order_uuid) + .ok_or(ErrorCode::SwiftOrderDoesNotExist)? + .order_id; + let taker_order = taker + .get_order(taker_order_id) + .ok_or(ErrorCode::TakerOrderNotFound)?; + + let remaining_accounts_iter = &mut ctx.remaining_accounts.iter().peekable(); + let AccountMaps { + perp_market_map, + spot_market_map, + mut oracle_map, + } = load_maps( + remaining_accounts_iter, + &BTreeSet::new(), + &BTreeSet::new(), + slot, + None, + )?; + + let oracle_price = oracle_map + .get_price_data( + &perp_market_map + .get_ref(&taker_order.market_index)? + .oracle_id(), + )? + .price; + + let (order_params, taker_base_asset_amount_unfilled, taker_price, maker_price) = process_order( + &maker, + &perp_market_map, + &spot_market_map, + taker_order, + slot, + params.max_position, + params.min_position, + oracle_price, + params.get_worst_price(oracle_price, taker_order.direction)?, + params.post_only.unwrap_or(PostOnlyParam::MustPostOnly), + )?; + + drop(taker); + drop(maker); + + place_and_make_swift(&ctx, order_params, params.swift_order_uuid)?; + + let taker = ctx.accounts.taker.load()?; + + let taker_base_asset_amount_unfilled_after = match taker.get_order(taker_order_id) { + Some(order) => order.get_base_asset_amount_unfilled(None)?, + None => 0, + }; + + if taker_base_asset_amount_unfilled_after == taker_base_asset_amount_unfilled { + // taker order failed to fill + msg!( + "taker price = {} maker price = {} oracle price = {}", + taker_price, + maker_price, + oracle_price + ); + msg!("jit params {:?}", params); + + let perp_market = perp_market_map.get_ref(&order_params.market_index)?; + let reserve_price = perp_market.amm.reserve_price()?; + let (bid_price, ask_price) = perp_market.amm.bid_ask_price(reserve_price)?; + msg!( + "vamm bid price = {} vamm ask price = {}", + bid_price, + ask_price + ); + + return Err(ErrorCode::NoFill.into()); + } + + Ok(()) +} + +#[inline(always)] +fn process_order( + maker: &User, + perp_market_map: &PerpMarketMap, + spot_market_map: &SpotMarketMap, + taker_order: &Order, + slot: u64, + max_position: i64, + min_position: i64, + oracle_price: i64, + maker_worst_price: u64, + post_only: PostOnlyParam, +) -> Result<(OrderParams, u64, u64, u64)> { + let market_type = taker_order.market_type; + let market_index = taker_order.market_index; let taker_direction = taker_order.direction; let slots_left = taker_order @@ -53,41 +237,24 @@ pub fn jit<'c: 'info, 'info>( taker_order.oracle_price_offset ); - let remaining_accounts_iter = &mut ctx.remaining_accounts.iter().peekable(); - let AccountMaps { - perp_market_map, - spot_market_map, - mut oracle_map, - } = load_maps( - remaining_accounts_iter, - &BTreeSet::new(), - &BTreeSet::new(), - slot, - None, - )?; + let (tick_size, min_order_size, is_prediction_market) = if market_type == DriftMarketType::Perp + { + let perp_market = perp_market_map.get_ref(&market_index)?; - let (oracle_price, tick_size, min_order_size, is_prediction_market) = - if market_type == DriftMarketType::Perp { - let perp_market = perp_market_map.get_ref(&market_index)?; - let oracle_price = oracle_map.get_price_data(&perp_market.oracle_id())?.price; - - ( - oracle_price, - perp_market.amm.order_tick_size, - perp_market.amm.min_order_size, - perp_market.is_prediction_market(), - ) - } else { - let spot_market = spot_market_map.get_ref(&market_index)?; - let oracle_price = oracle_map.get_price_data(&spot_market.oracle_id())?.price; - - ( - oracle_price, - spot_market.order_tick_size, - spot_market.min_order_size, - false, - ) - }; + ( + perp_market.amm.order_tick_size, + perp_market.amm.min_order_size, + perp_market.is_prediction_market(), + ) + } else { + let spot_market = spot_market_map.get_ref(&market_index)?; + + ( + spot_market.order_tick_size, + spot_market.min_order_size, + false, + ) + }; let taker_price = match taker_order.get_limit_price( Some(oracle_price), @@ -115,7 +282,6 @@ pub fn jit<'c: 'info, 'info>( }; let maker_direction = taker_direction.opposite(); - let maker_worst_price = params.get_worst_price(oracle_price, taker_direction)?; match maker_direction { PositionDirection::Long => { if taker_price > maker_worst_price { @@ -191,7 +357,8 @@ pub fn jit<'c: 'info, 'info>( }; let maker_base_asset_amount = match check_position_limits( - params, + max_position, + min_position, maker_direction, taker_base_asset_amount_unfilled, maker_existing_position, @@ -212,10 +379,7 @@ pub fn jit<'c: 'info, 'info>( price: maker_price, market_index, reduce_only: false, - post_only: params - .post_only - .unwrap_or(PostOnlyParam::MustPostOnly) - .to_drift_param(), + post_only, immediate_or_cancel: true, max_ts: None, trigger_price: None, @@ -225,42 +389,12 @@ pub fn jit<'c: 'info, 'info>( auction_start_price: None, auction_end_price: None, }; - - drop(taker); - drop(maker); - - place_and_make(&ctx, params.taker_order_id, order_params)?; - - let taker = ctx.accounts.taker.load()?; - - let taker_base_asset_amount_unfilled_after = match taker.get_order(params.taker_order_id) { - Some(order) => order.get_base_asset_amount_unfilled(None)?, - None => 0, - }; - - if taker_base_asset_amount_unfilled_after == taker_base_asset_amount_unfilled { - // taker order failed to fill - msg!( - "taker price = {} maker price = {} oracle price = {}", - taker_price, - maker_price, - oracle_price - ); - msg!("jit params {:?}", params); - if market_type == DriftMarketType::Perp { - let perp_market = perp_market_map.get_ref(&market_index)?; - let reserve_price = perp_market.amm.reserve_price()?; - let (bid_price, ask_price) = perp_market.amm.bid_ask_price(reserve_price)?; - msg!( - "vamm bid price = {} vamm ask price = {}", - bid_price, - ask_price - ); - } - return Err(ErrorCode::NoFill.into()); - } - - Ok(()) + Ok(( + order_params, + taker_base_asset_amount_unfilled, + taker_price, + maker_price, + )) } #[derive(Accounts)] @@ -278,6 +412,24 @@ pub struct Jit<'info> { pub drift_program: Program<'info, Drift>, } +#[derive(Accounts)] +pub struct JitSwift<'info> { + pub state: Box>, + #[account(mut)] + pub user: AccountLoader<'info, User>, + #[account(mut)] + pub user_stats: AccountLoader<'info, UserStats>, + #[account(mut)] + pub taker: AccountLoader<'info, User>, + #[account(mut)] + pub taker_stats: AccountLoader<'info, UserStats>, + /// CHECK: checked in SwiftUserOrdersZeroCopy checks + #[account(mut)] + pub taker_swift_user_orders: AccountInfo<'info>, + pub authority: Signer<'info>, + pub drift_program: Program<'info, Drift>, +} + #[derive(Debug, Clone, Copy, AnchorSerialize, AnchorDeserialize, PartialEq, Eq)] pub struct JitParams { pub taker_order_id: u32, @@ -322,21 +474,66 @@ impl JitParams { } } +#[derive(Debug, Clone, Copy, AnchorSerialize, AnchorDeserialize, PartialEq, Eq)] +pub struct JitSwiftParams { + pub swift_order_uuid: [u8; 8], + pub max_position: i64, + pub min_position: i64, + pub bid: i64, + pub ask: i64, + pub price_type: PriceType, + pub post_only: Option, +} + +impl Default for JitSwiftParams { + fn default() -> Self { + Self { + swift_order_uuid: [0; 8], + max_position: 0, + min_position: 0, + bid: 0, + ask: 0, + price_type: PriceType::Limit, + post_only: None, + } + } +} + +impl JitSwiftParams { + pub fn get_worst_price( + self, + oracle_price: i64, + taker_direction: PositionDirection, + ) -> DriftResult { + match (taker_direction, self.price_type) { + (PositionDirection::Long, PriceType::Limit) => Ok(self.ask.unsigned_abs()), + (PositionDirection::Short, PriceType::Limit) => Ok(self.bid.unsigned_abs()), + (PositionDirection::Long, PriceType::Oracle) => { + Ok(oracle_price.safe_add(self.ask)?.unsigned_abs()) + } + (PositionDirection::Short, PriceType::Oracle) => { + Ok(oracle_price.safe_add(self.bid)?.unsigned_abs()) + } + } + } +} + fn check_position_limits( - params: JitParams, + max_position: i64, + min_position: i64, maker_direction: PositionDirection, taker_base_asset_amount_unfilled: u64, maker_existing_position: i64, min_order_size: u64, ) -> Result { if maker_direction == PositionDirection::Long { - let size = params.max_position.safe_sub(maker_existing_position)?; + let size = max_position.safe_sub(maker_existing_position)?; if size <= min_order_size.cast()? { msg!( "maker existing position {} >= max position {} + min order size {}", maker_existing_position, - params.max_position, + max_position, min_order_size ); return Err(ErrorCode::PositionLimitBreached.into()); @@ -344,13 +541,13 @@ fn check_position_limits( Ok(size.unsigned_abs().min(taker_base_asset_amount_unfilled)) } else { - let size = maker_existing_position.safe_sub(params.min_position)?; + let size = maker_existing_position.safe_sub(min_position)?; if size <= min_order_size.cast()? { msg!( "maker existing position {} <= min position {} + min order size {}", maker_existing_position, - params.min_position, + min_position, min_order_size ); return Err(ErrorCode::PositionLimitBreached.into()); @@ -387,62 +584,175 @@ fn place_and_make<'info>( Ok(()) } +fn place_and_make_swift<'info>( + ctx: &Context<'_, '_, '_, 'info, JitSwift<'info>>, + order_params: OrderParams, + swift_order_uuid: [u8; 8], +) -> Result<()> { + let drift_program = ctx.accounts.drift_program.to_account_info(); + let state = ctx.accounts.state.to_account_info(); + let taker = ctx.accounts.taker.to_account_info(); + let taker_stats = ctx.accounts.taker_stats.to_account_info(); + let taker_swift_user_orders = ctx.accounts.taker_swift_user_orders.to_account_info(); + + let cpi_accounts_place_and_make = PlaceAndMakeSwift { + state, + user: ctx.accounts.user.to_account_info().clone(), + user_stats: ctx.accounts.user_stats.to_account_info().clone(), + authority: ctx.accounts.authority.to_account_info().clone(), + taker, + taker_stats, + taker_swift_user_orders, + }; + + let cpi_context_place_and_make = CpiContext::new(drift_program, cpi_accounts_place_and_make) + .with_remaining_accounts(ctx.remaining_accounts.into()); + + drift::cpi::place_and_make_swift_perp_order( + cpi_context_place_and_make, + order_params, + swift_order_uuid, + )?; + Ok(()) +} + #[cfg(test)] mod tests { use super::*; #[test] fn test_check_position_limits() { - let params = JitParams { - max_position: 100, - min_position: -100, - ..Default::default() - }; + let max_position: i64 = 100; + let min_position: i64 = -100; // same direction, doesn't breach - let result = check_position_limits(params, PositionDirection::Long, 10, 40, 0); + let result = check_position_limits( + max_position, + min_position, + PositionDirection::Long, + 10, + 40, + 0, + ); assert!(result.is_ok()); assert_eq!(result.unwrap(), 10); - let result = check_position_limits(params, PositionDirection::Short, 10, -40, 0); + let result = check_position_limits( + max_position, + min_position, + PositionDirection::Short, + 10, + -40, + 0, + ); assert!(result.is_ok()); assert_eq!(result.unwrap(), 10); // same direction, whole order breaches, only takes enough to hit limit - let result = check_position_limits(params, PositionDirection::Long, 100, 40, 0); + let result = check_position_limits( + max_position, + min_position, + PositionDirection::Long, + 100, + 40, + 0, + ); assert!(result.is_ok()); assert_eq!(result.unwrap(), 60); - let result = check_position_limits(params, PositionDirection::Short, 100, -40, 0); + let result = check_position_limits( + max_position, + min_position, + PositionDirection::Short, + 100, + -40, + 0, + ); assert!(result.is_ok()); assert_eq!(result.unwrap(), 60); // opposite direction, doesn't breach - let result = check_position_limits(params, PositionDirection::Long, 10, -40, 0); + let result = check_position_limits( + max_position, + min_position, + PositionDirection::Long, + 10, + -40, + 0, + ); assert!(result.is_ok()); assert_eq!(result.unwrap(), 10); - let result = check_position_limits(params, PositionDirection::Short, 10, 40, 0); + let result = check_position_limits( + max_position, + min_position, + PositionDirection::Short, + 10, + 40, + 0, + ); assert!(result.is_ok()); assert_eq!(result.unwrap(), 10); // opposite direction, whole order breaches, only takes enough to take flipped limit - let result = check_position_limits(params, PositionDirection::Long, 200, -40, 0); + let result = check_position_limits( + max_position, + min_position, + PositionDirection::Long, + 200, + -40, + 0, + ); assert!(result.is_ok()); assert_eq!(result.unwrap(), 140); - let result = check_position_limits(params, PositionDirection::Short, 200, 40, 0); + let result = check_position_limits( + max_position, + min_position, + PositionDirection::Short, + 200, + 40, + 0, + ); assert!(result.is_ok()); assert_eq!(result.unwrap(), 140); // opposite direction, maker already breached, allows reducing - let result = check_position_limits(params, PositionDirection::Long, 200, -150, 0); + let result = check_position_limits( + max_position, + min_position, + PositionDirection::Long, + 200, + -150, + 0, + ); assert!(result.is_ok()); assert_eq!(result.unwrap(), 200); - let result = check_position_limits(params, PositionDirection::Short, 200, 150, 0); + let result = check_position_limits( + max_position, + min_position, + PositionDirection::Short, + 200, + 150, + 0, + ); assert!(result.is_ok()); assert_eq!(result.unwrap(), 200); // same direction, maker already breached, errors - let result = check_position_limits(params, PositionDirection::Long, 200, 150, 0); + let result = check_position_limits( + max_position, + min_position, + PositionDirection::Long, + 200, + 150, + 0, + ); assert!(result.is_err()); - let result = check_position_limits(params, PositionDirection::Short, 200, -150, 0); + let result = check_position_limits( + max_position, + min_position, + PositionDirection::Short, + 200, + -150, + 0, + ); assert!(result.is_err()); } } diff --git a/programs/jit-proxy/src/lib.rs b/programs/jit-proxy/src/lib.rs index 743377b3..62b742ab 100644 --- a/programs/jit-proxy/src/lib.rs +++ b/programs/jit-proxy/src/lib.rs @@ -19,6 +19,13 @@ pub mod jit_proxy { instructions::jit(ctx, params) } + pub fn jit_swift<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, JitSwift<'info>>, + params: JitSwiftParams, + ) -> Result<()> { + instructions::jit_swift(ctx, params) + } + pub fn check_order_constraints<'c: 'info, 'info>( ctx: Context<'_, '_, 'c, 'info, CheckOrderConstraints<'info>>, constraints: Vec, diff --git a/ts/sdk/package.json b/ts/sdk/package.json index d7243c76..391e0a1c 100644 --- a/ts/sdk/package.json +++ b/ts/sdk/package.json @@ -7,6 +7,7 @@ }, "dependencies": { "@coral-xyz/anchor": "0.26.0", + "tweetnacl-util": "^0.15.1", "@drift-labs/sdk": "2.108.0-beta.4", "@solana/web3.js": "1.91.7" }, diff --git a/ts/sdk/src/jitProxyClient.ts b/ts/sdk/src/jitProxyClient.ts index 2a14df48..a136d149 100644 --- a/ts/sdk/src/jitProxyClient.ts +++ b/ts/sdk/src/jitProxyClient.ts @@ -1,6 +1,7 @@ import { BN, DriftClient, + getSwiftUserAccountPublicKey, isVariant, MakerInfo, MarketType, @@ -14,6 +15,7 @@ import { IDL, JitProxy } from './types/jit_proxy'; import { PublicKey, TransactionInstruction } from '@solana/web3.js'; import { Program } from '@coral-xyz/anchor'; import { TxSigAndSlot } from '@drift-labs/sdk'; +import { SignedSwiftOrderParams } from '@drift-labs/sdk/lib/node/swift/types'; export type JitIxParams = { takerKey: PublicKey; @@ -30,6 +32,13 @@ export type JitIxParams = { subAccountId?: number; }; +export type JitSwiftIxParams = JitIxParams & { + authorityToUse: PublicKey; + signedSwiftOrderParams: SignedSwiftOrderParams; + uuid: Uint8Array; + marketIndex: number; +}; + export class PriceType { static readonly LIMIT = { limit: {} }; static readonly ORACLE = { oracle: {} }; @@ -66,6 +75,31 @@ export class JitProxyClient { return await this.driftClient.sendTransaction(tx); } + public async jitSwift( + params: JitSwiftIxParams, + txParams?: TxParams, + precedingIxs?: TransactionInstruction[] + ): Promise { + const swiftTakerIxs = await this.driftClient.getPlaceSwiftTakerPerpOrderIxs( + params.signedSwiftOrderParams, + params.marketIndex, + { + taker: params.takerKey, + takerStats: params.takerStatsKey, + takerUserAccount: params.taker, + }, + params.authorityToUse, + precedingIxs + ); + + const ix = await this.getJitSwiftIx(params); + const tx = await this.driftClient.buildTransaction( + [...swiftTakerIxs, ix], + txParams + ); + return await this.driftClient.sendTransaction(tx); + } + public async getJitIx({ takerKey, takerStatsKey, @@ -145,6 +179,73 @@ export class JitProxyClient { .instruction(); } + public async getJitSwiftIx({ + takerKey, + takerStatsKey, + taker, + maxPosition, + minPosition, + bid, + ask, + postOnly = null, + priceType = PriceType.LIMIT, + referrerInfo, + subAccountId, + uuid, + marketIndex, + }: JitSwiftIxParams): Promise { + subAccountId = + subAccountId !== undefined + ? subAccountId + : this.driftClient.activeSubAccountId; + const remainingAccounts = this.driftClient.getRemainingAccounts({ + userAccounts: [taker, this.driftClient.getUserAccount(subAccountId)], + writableSpotMarketIndexes: [], + writablePerpMarketIndexes: [marketIndex], + }); + + if (referrerInfo) { + remainingAccounts.push({ + pubkey: referrerInfo.referrer, + isWritable: true, + isSigner: false, + }); + remainingAccounts.push({ + pubkey: referrerInfo.referrerStats, + isWritable: true, + isSigner: false, + }); + } + + const jitSwiftParams = { + swiftOrderUuid: Array.from(uuid), + maxPosition, + minPosition, + bid, + ask, + postOnly, + priceType, + }; + + return this.program.methods + .jitSwift(jitSwiftParams) + .accounts({ + taker: takerKey, + takerStats: takerStatsKey, + takerSwiftUserOrders: getSwiftUserAccountPublicKey( + this.driftClient.program.programId, + taker.authority + ), + authority: this.driftClient.wallet.payer.publicKey, + state: await this.driftClient.getStatePublicKey(), + user: await this.driftClient.getUserAccountPublicKey(subAccountId), + userStats: this.driftClient.getUserStatsAccountPublicKey(), + driftProgram: this.driftClient.program.programId, + }) + .remainingAccounts(remainingAccounts) + .instruction(); + } + public async getCheckOrderConstraintIx({ subAccountId, orderConstraints, diff --git a/ts/sdk/src/jitter/baseJitter.ts b/ts/sdk/src/jitter/baseJitter.ts index 2295bf13..6eb9896d 100644 --- a/ts/sdk/src/jitter/baseJitter.ts +++ b/ts/sdk/src/jitter/baseJitter.ts @@ -6,14 +6,27 @@ import { BN, BulkAccountLoader, DriftClient, + getAuctionPrice, + getUserAccountPublicKey, getUserStatsAccountPublicKey, hasAuctionPrice, isVariant, + MarketType, Order, + OrderStatus, + OrderTriggerCondition, + OrderType, + PositionDirection, PostOnlyParams, + SlotSubscriber, + SwiftOrderParamsMessage, + SwiftOrderSubscriber, UserAccount, UserStatsMap, + ZERO, } from '@drift-labs/sdk'; +import { SignedSwiftOrderParams } from '@drift-labs/sdk/lib/node/swift/types'; +import { decodeUTF8 } from 'tweetnacl-util'; export type UserFilter = ( userAccount: UserAccount, @@ -33,6 +46,8 @@ export type JitParams = { export abstract class BaseJitter { auctionSubscriber: AuctionSubscriber; + swiftOrderSubscriber: SwiftOrderSubscriber; + slotSubscriber: SlotSubscriber; driftClient: DriftClient; jitProxyClient: JitProxyClient; userStatsMap: UserStatsMap; @@ -53,11 +68,15 @@ export abstract class BaseJitter { jitProxyClient, driftClient, userStatsMap, + swiftOrderSubscriber, + slotSubscriber, }: { driftClient: DriftClient; auctionSubscriber: AuctionSubscriber; jitProxyClient: JitProxyClient; userStatsMap: UserStatsMap; + swiftOrderSubscriber?: SwiftOrderSubscriber; + slotSubscriber?: SlotSubscriber; }) { this.auctionSubscriber = auctionSubscriber; this.driftClient = driftClient; @@ -68,6 +87,12 @@ export abstract class BaseJitter { this.driftClient, new BulkAccountLoader(this.driftClient.connection, 'confirmed', 0) ); + this.slotSubscriber = slotSubscriber; + this.swiftOrderSubscriber = swiftOrderSubscriber; + + if (this.swiftOrderSubscriber && !this.slotSubscriber) { + throw new Error('Slot subscriber is required for swift order subscriber'); + } } async subscribe(): Promise { @@ -164,6 +189,127 @@ export abstract class BaseJitter { } } ); + await this.slotSubscriber?.subscribe(); + await this.swiftOrderSubscriber?.subscribe( + async (orderMessageRaw, swiftOrderParamsMessage) => { + const swiftOrderParamsBufHex = Buffer.from( + orderMessageRaw['order_message'] + ); + const swiftOrderParamsBuf = Buffer.from( + orderMessageRaw['order_message'], + 'hex' + ); + const { + swiftOrderParams, + subAccountId: takerSubaccountId, + }: SwiftOrderParamsMessage = + this.driftClient.decodeSwiftOrderParamsMessage(swiftOrderParamsBuf); + + const takerAuthority = new PublicKey( + orderMessageRaw['taker_authority'] + ); + const takerUserPubkey = await getUserAccountPublicKey( + this.driftClient.program.programId, + takerAuthority, + takerSubaccountId + ); + const takerUserPubkeyString = takerUserPubkey.toBase58(); + const takerUserAccount = ( + await this.swiftOrderSubscriber.userMap.mustGet( + takerUserPubkey.toString() + ) + ).getUserAccount(); + + const swiftOrder: Order = { + status: OrderStatus.OPEN, + orderType: swiftOrderParams.orderType, + orderId: this.convertUuidToNumber(orderMessageRaw['uuid']), + slot: swiftOrderParamsMessage.slot, + marketIndex: swiftOrderParams.marketIndex, + marketType: MarketType.PERP, + baseAssetAmount: swiftOrderParams.baseAssetAmount, + auctionDuration: swiftOrderParams.auctionDuration!, + auctionStartPrice: swiftOrderParams.auctionStartPrice!, + auctionEndPrice: swiftOrderParams.auctionEndPrice!, + immediateOrCancel: swiftOrderParams.immediateOrCancel, + direction: swiftOrderParams.direction, + postOnly: false, + oraclePriceOffset: swiftOrderParams.oraclePriceOffset ?? 0, + maxTs: swiftOrderParams.maxTs ?? ZERO, + reduceOnly: swiftOrderParams.reduceOnly, + triggerCondition: swiftOrderParams.triggerCondition, + // Rest are not necessary and set for type conforming + price: ZERO, + existingPositionDirection: PositionDirection.LONG, + triggerPrice: ZERO, + baseAssetAmountFilled: ZERO, + quoteAssetAmountFilled: ZERO, + quoteAssetAmount: ZERO, + userOrderId: 0, + }; + swiftOrder.price = getAuctionPrice( + swiftOrder, + this.slotSubscriber?.getSlot(), + this.driftClient.getOracleDataForPerpMarket(swiftOrder.marketIndex) + .price + ); + + if (this.userFilter) { + if ( + this.userFilter(takerUserAccount, takerUserPubkeyString, swiftOrder) + ) { + return; + } + } + + const orderSignature = this.getOrderSignatures( + takerUserPubkeyString, + swiftOrder.orderId + ); + + if (this.seenOrders.has(orderSignature)) { + return; + } + this.seenOrders.add(orderSignature); + + if (this.onGoingAuctions.has(orderSignature)) { + return; + } + + if (!this.perpParams.has(swiftOrder.marketIndex)) { + return; + } + + const perpMarketAccount = this.driftClient.getPerpMarketAccount( + swiftOrder.marketIndex + ); + if (swiftOrder.baseAssetAmount.lt(perpMarketAccount.amm.minOrderSize)) { + return; + } + + const promise = this.createTrySwiftFill( + takerAuthority, + { + orderParams: swiftOrderParamsBufHex, + signature: Buffer.from( + orderMessageRaw['order_signature'], + 'base64' + ), + }, + decodeUTF8(orderMessageRaw['uuid']), + takerUserAccount, + takerUserPubkey, + getUserStatsAccountPublicKey( + this.driftClient.program.programId, + takerUserAccount.authority + ), + swiftOrder, + orderSignature, + orderMessageRaw['market_index'] + ).bind(this)(); + this.onGoingAuctions.set(orderSignature, promise); + } + ); } createTryFill( @@ -176,6 +322,20 @@ export abstract class BaseJitter { throw new Error('Not implemented'); } + createTrySwiftFill( + authorityToUse: PublicKey, + signedSwiftOrderParams: SignedSwiftOrderParams, + uuid: Uint8Array, + taker: UserAccount, + takerKey: PublicKey, + takerStatsKey: PublicKey, + order: Order, + orderSignature: string, + marketIndex: number + ): () => Promise { + throw new Error('Not implemented'); + } + deleteOnGoingAuction(orderSignature: string): void { this.onGoingAuctions.delete(orderSignature); this.seenOrders.delete(orderSignature); @@ -185,6 +345,19 @@ export abstract class BaseJitter { return `${takerKey}-${orderId}`; } + private convertUuidToNumber(uuid: string): number { + return uuid + .split('') + .reduce( + (n, c) => + n * 64 + + '_~0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'.indexOf( + c + ), + 0 + ); + } + public updatePerpParams(marketIndex: number, params: JitParams): void { this.perpParams.set(marketIndex, params); } diff --git a/ts/sdk/src/jitter/jitterShotgun.ts b/ts/sdk/src/jitter/jitterShotgun.ts index 5fc0a6df..fe51a271 100644 --- a/ts/sdk/src/jitter/jitterShotgun.ts +++ b/ts/sdk/src/jitter/jitterShotgun.ts @@ -5,10 +5,13 @@ import { DriftClient, Order, PostOnlyParams, + SlotSubscriber, + SwiftOrderSubscriber, UserAccount, UserStatsMap, } from '@drift-labs/sdk'; import { BaseJitter } from './baseJitter'; +import { SignedSwiftOrderParams } from '@drift-labs/sdk/lib/node/swift/types'; export class JitterShotgun extends BaseJitter { constructor({ @@ -16,17 +19,23 @@ export class JitterShotgun extends BaseJitter { jitProxyClient, driftClient, userStatsMap, + swiftOrderSubscriber, + slotSubscriber, }: { driftClient: DriftClient; auctionSubscriber: AuctionSubscriber; jitProxyClient: JitProxyClient; userStatsMap?: UserStatsMap; + swiftOrderSubscriber?: SwiftOrderSubscriber; + slotSubscriber?: SlotSubscriber; }) { super({ auctionSubscriber, jitProxyClient, driftClient, userStatsMap, + swiftOrderSubscriber, + slotSubscriber, }); } @@ -104,6 +113,90 @@ export class JitterShotgun extends BaseJitter { this.deleteOnGoingAuction(orderSignature); }; } + + createTrySwiftFill( + authorityToUse: PublicKey, + signedSwiftOrderParams: SignedSwiftOrderParams, + uuid: Uint8Array, + taker: UserAccount, + takerKey: PublicKey, + takerStatsKey: PublicKey, + order: Order, + orderSignature: string, + marketIndex: number + ): () => Promise { + return async () => { + let i = 0; + + const takerStats = await this.userStatsMap.mustGet( + taker.authority.toString() + ); + const referrerInfo = takerStats.getReferrerInfo(); + + // assumes each preflight simulation takes ~1 slot + while (i < order.auctionDuration) { + const params = this.perpParams.get(order.marketIndex); + if (!params) { + this.deleteOnGoingAuction(orderSignature); + return; + } + + const txParams = { + computeUnits: this.computeUnits, + computeUnitsPrice: this.computeUnitsPrice, + }; + + console.log(`Trying to fill ${orderSignature}`); + try { + const { txSig } = await this.jitProxyClient.jitSwift( + { + takerKey, + takerStatsKey, + taker, + takerOrderId: order.orderId, + maxPosition: params.maxPosition, + minPosition: params.minPosition, + bid: params.bid, + ask: params.ask, + postOnly: params.postOnlyParams ?? PostOnlyParams.MUST_POST_ONLY, + priceType: params.priceType, + referrerInfo, + subAccountId: params.subAccountId, + authorityToUse, + signedSwiftOrderParams, + uuid, + marketIndex, + }, + txParams + ); + + console.log( + `Successfully sent tx for ${orderSignature} txSig ${txSig}` + ); + await sleep(10000); + this.deleteOnGoingAuction(orderSignature); + return; + } catch (e) { + console.error(`Failed to fill ${orderSignature}`); + console.log(e); + if (e.message.includes('0x1770') || e.message.includes('0x1771')) { + console.log('Order does not cross params yet, retrying'); + } else if (e.message.includes('0x1779')) { + console.log('Order could not fill'); + } else if (e.message.includes('0x1793')) { + console.log('Oracle invalid, retrying'); + } else { + await sleep(10000); + this.deleteOnGoingAuction(orderSignature); + return; + } + } + i++; + } + + this.deleteOnGoingAuction(orderSignature); + }; + } } function sleep(ms: number): Promise { diff --git a/ts/sdk/src/jitter/jitterSniper.ts b/ts/sdk/src/jitter/jitterSniper.ts index da836a51..36a50e0c 100644 --- a/ts/sdk/src/jitter/jitterSniper.ts +++ b/ts/sdk/src/jitter/jitterSniper.ts @@ -14,11 +14,13 @@ import { PostOnlyParams, PRICE_PRECISION, SlotSubscriber, + SwiftOrderSubscriber, UserAccount, UserStatsMap, ZERO, } from '@drift-labs/sdk'; import { BaseJitter } from './baseJitter'; +import { SignedSwiftOrderParams } from '@drift-labs/sdk/lib/node/swift/types'; type AuctionAndOrderDetails = { slotsTilCross: number; @@ -41,18 +43,22 @@ export class JitterSniper extends BaseJitter { jitProxyClient, driftClient, userStatsMap, + swiftOrderSubscriber, }: { driftClient: DriftClient; slotSubscriber: SlotSubscriber; auctionSubscriber: AuctionSubscriber; jitProxyClient: JitProxyClient; userStatsMap?: UserStatsMap; + swiftOrderSubscriber?: SwiftOrderSubscriber; }) { super({ auctionSubscriber, jitProxyClient, driftClient, userStatsMap, + swiftOrderSubscriber, + slotSubscriber, }); this.slotSubscriber = slotSubscriber; } @@ -239,6 +245,196 @@ export class JitterSniper extends BaseJitter { }; } + createTrySwiftFill( + authorityToUse: PublicKey, + signedSwiftOrderParams: SignedSwiftOrderParams, + uuid: Uint8Array, + taker: UserAccount, + takerKey: PublicKey, + takerStatsKey: PublicKey, + order: Order, + orderSignature: string, + marketIndex: number + ): () => Promise { + return async () => { + const params = this.perpParams.get(order.marketIndex); + if (!params) { + this.deleteOnGoingAuction(orderSignature); + return; + } + + const takerStats = await this.userStatsMap.mustGet( + taker.authority.toString() + ); + const referrerInfo = takerStats.getReferrerInfo(); + + const { + slotsTilCross, + willCross, + bid, + ask, + auctionStartPrice, + auctionEndPrice, + stepSize, + oraclePrice, + } = this.getAuctionAndOrderDetails(order); + + // don't increase risk if we're past max positions + if (isVariant(order.marketType, 'perp')) { + const currPerpPos = + this.driftClient.getUser().getPerpPosition(order.marketIndex) || + this.driftClient.getUser().getEmptyPosition(order.marketIndex); + if ( + currPerpPos.baseAssetAmount.lt(ZERO) && + isVariant(order.direction, 'short') + ) { + if (currPerpPos.baseAssetAmount.lte(params.minPosition)) { + console.log( + `Order would increase existing short (mkt ${getVariant( + order.marketType + )}-${order.marketIndex}) too much` + ); + this.deleteOnGoingAuction(orderSignature); + return; + } + } else if ( + currPerpPos.baseAssetAmount.gt(ZERO) && + isVariant(order.direction, 'long') + ) { + if (currPerpPos.baseAssetAmount.gte(params.maxPosition)) { + console.log( + `Order would increase existing long (mkt ${getVariant( + order.marketType + )}-${order.marketIndex}) too much` + ); + this.deleteOnGoingAuction(orderSignature); + return; + } + } + } + + console.log(` + Taker wants to ${JSON.stringify( + order.direction + )}, order slot is ${order.slot.toNumber()}, + My market: ${bid}@${ask}, + Auction: ${auctionStartPrice} -> ${auctionEndPrice}, step size ${stepSize} + Current slot: ${ + this.slotSubscriber.currentSlot + }, Order slot: ${order.slot.toNumber()}, + Will cross?: ${willCross} + Slots to wait: ${slotsTilCross}. Target slot = ${ + order.slot.toNumber() + slotsTilCross + } + `); + + this.waitForSlotOrCrossOrExpiry( + willCross + ? order.slot.toNumber() + slotsTilCross + : order.slot.toNumber() + order.auctionDuration + 1, + order, + { + slotsTilCross, + willCross, + bid, + ask, + auctionStartPrice, + auctionEndPrice, + stepSize, + oraclePrice, + } + ).then(async ({ slot, updatedDetails }) => { + if (slot === -1) { + console.log('Auction expired without crossing'); + this.deleteOnGoingAuction(orderSignature); + return; + } + + const params = isVariant(order.marketType, 'perp') + ? this.perpParams.get(order.marketIndex) + : this.spotParams.get(order.marketIndex); + const bid = isVariant(params.priceType, 'oracle') + ? convertToNumber(oraclePrice.price.add(params.bid), PRICE_PRECISION) + : convertToNumber(params.bid, PRICE_PRECISION); + const ask = isVariant(params.priceType, 'oracle') + ? convertToNumber(oraclePrice.price.add(params.ask), PRICE_PRECISION) + : convertToNumber(params.ask, PRICE_PRECISION); + const auctionPrice = convertToNumber( + getAuctionPrice(order, slot, updatedDetails.oraclePrice.price), + PRICE_PRECISION + ); + console.log(` + Expected auction price: ${auctionStartPrice + slotsTilCross * stepSize} + Actual auction price: ${auctionPrice} + ----------------- + Looking for slot ${order.slot.toNumber() + slotsTilCross} + Got slot ${slot} + `); + + console.log(`Trying to fill ${orderSignature} with: + market: ${bid}@${ask} + auction price: ${auctionPrice} + submitting" ${convertToNumber(params.bid, PRICE_PRECISION)}@${convertToNumber( + params.ask, + PRICE_PRECISION + )} + `); + let i = 0; + while (i < 10) { + try { + const txParams = { + computeUnits: this.computeUnits, + computeUnitsPrice: this.computeUnitsPrice, + }; + const { txSig } = await this.jitProxyClient.jitSwift( + { + takerKey, + takerStatsKey, + taker, + takerOrderId: order.orderId, + maxPosition: params.maxPosition, + minPosition: params.minPosition, + bid: params.bid, + ask: params.ask, + postOnly: + params.postOnlyParams ?? PostOnlyParams.MUST_POST_ONLY, + priceType: params.priceType, + referrerInfo, + subAccountId: params.subAccountId, + authorityToUse, + signedSwiftOrderParams, + uuid, + marketIndex, + }, + txParams + ); + + console.log(`Filled ${orderSignature} txSig ${txSig}`); + await sleep(3000); + this.deleteOnGoingAuction(orderSignature); + return; + } catch (e) { + console.error(`Failed to fill ${orderSignature}`); + if (e.message.includes('0x1770') || e.message.includes('0x1771')) { + console.log('Order does not cross params yet'); + } else if (e.message.includes('0x1779')) { + console.log('Order could not fill'); + } else if (e.message.includes('0x1793')) { + console.log('Oracle invalid'); + } else { + await sleep(3000); + this.deleteOnGoingAuction(orderSignature); + return; + } + } + await sleep(200); + i++; + } + }); + this.deleteOnGoingAuction(orderSignature); + }; + } + getAuctionAndOrderDetails(order: Order): AuctionAndOrderDetails { // Find number of slots until the order is expected to be in cross const params = isVariant(order.marketType, 'perp') diff --git a/ts/sdk/src/types/jit_proxy.ts b/ts/sdk/src/types/jit_proxy.ts index 9193b5d2..c5b47f12 100644 --- a/ts/sdk/src/types/jit_proxy.ts +++ b/ts/sdk/src/types/jit_proxy.ts @@ -1,5 +1,5 @@ export type JitProxy = { - version: '0.10.2'; + version: '0.12.0'; name: 'jit_proxy'; instructions: [ { @@ -50,6 +50,59 @@ export type JitProxy = { } ]; }, + { + name: 'jitSwift'; + accounts: [ + { + name: 'state'; + isMut: false; + isSigner: false; + }, + { + name: 'user'; + isMut: true; + isSigner: false; + }, + { + name: 'userStats'; + isMut: true; + isSigner: false; + }, + { + name: 'taker'; + isMut: true; + isSigner: false; + }, + { + name: 'takerStats'; + isMut: true; + isSigner: false; + }, + { + name: 'takerSwiftUserOrders'; + isMut: true; + isSigner: false; + }, + { + name: 'authority'; + isMut: false; + isSigner: true; + }, + { + name: 'driftProgram'; + isMut: false; + isSigner: false; + } + ]; + args: [ + { + name: 'params'; + type: { + defined: 'JitSwiftParams'; + }; + } + ]; + }, { name: 'checkOrderConstraints'; accounts: [ @@ -176,6 +229,50 @@ export type JitProxy = { ]; }; }, + { + name: 'JitSwiftParams'; + type: { + kind: 'struct'; + fields: [ + { + name: 'swiftOrderUuid'; + type: { + array: ['u8', 8]; + }; + }, + { + name: 'maxPosition'; + type: 'i64'; + }, + { + name: 'minPosition'; + type: 'i64'; + }, + { + name: 'bid'; + type: 'i64'; + }, + { + name: 'ask'; + type: 'i64'; + }, + { + name: 'priceType'; + type: { + defined: 'PriceType'; + }; + }, + { + name: 'postOnly'; + type: { + option: { + defined: 'PostOnlyParam'; + }; + }; + } + ]; + }; + }, { name: 'PostOnlyParam'; type: { @@ -275,12 +372,17 @@ export type JitProxy = { code: 6009; name: 'NoFill'; msg: 'NoFill'; + }, + { + code: 6010; + name: 'SwiftOrderDoesNotExist'; + msg: 'SwiftOrderDoesNotExist'; } ]; }; export const IDL: JitProxy = { - version: '0.10.2', + version: '0.12.0', name: 'jit_proxy', instructions: [ { @@ -331,6 +433,59 @@ export const IDL: JitProxy = { }, ], }, + { + name: 'jitSwift', + accounts: [ + { + name: 'state', + isMut: false, + isSigner: false, + }, + { + name: 'user', + isMut: true, + isSigner: false, + }, + { + name: 'userStats', + isMut: true, + isSigner: false, + }, + { + name: 'taker', + isMut: true, + isSigner: false, + }, + { + name: 'takerStats', + isMut: true, + isSigner: false, + }, + { + name: 'takerSwiftUserOrders', + isMut: true, + isSigner: false, + }, + { + name: 'authority', + isMut: false, + isSigner: true, + }, + { + name: 'driftProgram', + isMut: false, + isSigner: false, + }, + ], + args: [ + { + name: 'params', + type: { + defined: 'JitSwiftParams', + }, + }, + ], + }, { name: 'checkOrderConstraints', accounts: [ @@ -457,6 +612,50 @@ export const IDL: JitProxy = { ], }, }, + { + name: 'JitSwiftParams', + type: { + kind: 'struct', + fields: [ + { + name: 'swiftOrderUuid', + type: { + array: ['u8', 8], + }, + }, + { + name: 'maxPosition', + type: 'i64', + }, + { + name: 'minPosition', + type: 'i64', + }, + { + name: 'bid', + type: 'i64', + }, + { + name: 'ask', + type: 'i64', + }, + { + name: 'priceType', + type: { + defined: 'PriceType', + }, + }, + { + name: 'postOnly', + type: { + option: { + defined: 'PostOnlyParam', + }, + }, + }, + ], + }, + }, { name: 'PostOnlyParam', type: { @@ -557,5 +756,10 @@ export const IDL: JitProxy = { name: 'NoFill', msg: 'NoFill', }, + { + code: 6010, + name: 'SwiftOrderDoesNotExist', + msg: 'SwiftOrderDoesNotExist', + }, ], }; diff --git a/ts/sdk/yarn.lock b/ts/sdk/yarn.lock index 936a7fe6..39702d2d 100644 --- a/ts/sdk/yarn.lock +++ b/ts/sdk/yarn.lock @@ -1904,7 +1904,7 @@ tslib@^2.4.0: resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.3.tgz#0438f810ad7a9edcde7a241c3d80db693c8cbfe0" integrity sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ== -tweetnacl-util@0.15.1: +tweetnacl-util@0.15.1, tweetnacl-util@^0.15.1: version "0.15.1" resolved "https://registry.yarnpkg.com/tweetnacl-util/-/tweetnacl-util-0.15.1.tgz#b80fcdb5c97bcc508be18c44a4be50f022eea00b" integrity sha512-RKJBIj8lySrShN4w6i/BonWp2Z/uxwC3h4y7xsRrpP59ZboCd0GpEVsOnMDYLMmKBpYhb5TgHzZXy7wTfYFBRw==