Merge remote-tracking branch 'origin/dev' into ckamm/insurance

This commit is contained in:
Christian Kamm 2024-04-22 11:59:15 +02:00
commit 35d680325a
100 changed files with 12029 additions and 1336 deletions

View File

@ -32,7 +32,7 @@ on:
env:
CARGO_TERM_COLOR: always
SOLANA_VERSION: '1.16.14'
RUST_TOOLCHAIN: '1.69.0'
RUST_TOOLCHAIN: '1.70.0'
LOG_PROGRAM: '4MangoMjqJ2firMokCjjGgoK8d4MXcrgL7XJaL3w6fVg'
jobs:

327
Cargo.lock generated
View File

@ -119,7 +119,7 @@ version = "0.28.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "faa5be5b72abea167f87c868379ba3c2be356bfca9e6f474fd055fa0f7eeb4f2"
dependencies = [
"anchor-syn",
"anchor-syn 0.28.0",
"anyhow",
"proc-macro2 1.0.67",
"quote 1.0.33",
@ -127,13 +127,25 @@ dependencies = [
"syn 1.0.109",
]
[[package]]
name = "anchor-attribute-access-control"
version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5f619f1d04f53621925ba8a2e633ba5a6081f2ae14758cbb67f38fd823e0a3e"
dependencies = [
"anchor-syn 0.29.0",
"proc-macro2 1.0.67",
"quote 1.0.33",
"syn 1.0.109",
]
[[package]]
name = "anchor-attribute-account"
version = "0.28.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f468970344c7c9f9d03b4da854fd7c54f21305059f53789d0045c1dd803f0018"
dependencies = [
"anchor-syn",
"anchor-syn 0.28.0",
"anyhow",
"bs58 0.5.0",
"proc-macro2 1.0.67",
@ -142,62 +154,120 @@ dependencies = [
"syn 1.0.109",
]
[[package]]
name = "anchor-attribute-account"
version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7f2a3e1df4685f18d12a943a9f2a7456305401af21a07c9fe076ef9ecd6e400"
dependencies = [
"anchor-syn 0.29.0",
"bs58 0.5.0",
"proc-macro2 1.0.67",
"quote 1.0.33",
"syn 1.0.109",
]
[[package]]
name = "anchor-attribute-constant"
version = "0.28.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59948e7f9ef8144c2aefb3f32a40c5fce2798baeec765ba038389e82301017ef"
dependencies = [
"anchor-syn",
"anchor-syn 0.28.0",
"proc-macro2 1.0.67",
"syn 1.0.109",
]
[[package]]
name = "anchor-attribute-constant"
version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9423945cb55627f0b30903288e78baf6f62c6c8ab28fb344b6b25f1ffee3dca7"
dependencies = [
"anchor-syn 0.29.0",
"quote 1.0.33",
"syn 1.0.109",
]
[[package]]
name = "anchor-attribute-error"
version = "0.28.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc753c9d1c7981cb8948cf7e162fb0f64558999c0413058e2d43df1df5448086"
dependencies = [
"anchor-syn",
"anchor-syn 0.28.0",
"proc-macro2 1.0.67",
"quote 1.0.33",
"syn 1.0.109",
]
[[package]]
name = "anchor-attribute-error"
version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93ed12720033cc3c3bf3cfa293349c2275cd5ab99936e33dd4bf283aaad3e241"
dependencies = [
"anchor-syn 0.29.0",
"quote 1.0.33",
"syn 1.0.109",
]
[[package]]
name = "anchor-attribute-event"
version = "0.28.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f38b4e172ba1b52078f53fdc9f11e3dc0668ad27997838a0aad2d148afac8c97"
dependencies = [
"anchor-syn",
"anchor-syn 0.28.0",
"anyhow",
"proc-macro2 1.0.67",
"quote 1.0.33",
"syn 1.0.109",
]
[[package]]
name = "anchor-attribute-event"
version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eef4dc0371eba2d8c8b54794b0b0eb786a234a559b77593d6f80825b6d2c77a2"
dependencies = [
"anchor-syn 0.29.0",
"proc-macro2 1.0.67",
"quote 1.0.33",
"syn 1.0.109",
]
[[package]]
name = "anchor-attribute-program"
version = "0.28.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4eebd21543606ab61e2d83d9da37d24d3886a49f390f9c43a1964735e8c0f0d5"
dependencies = [
"anchor-syn",
"anchor-syn 0.28.0",
"anyhow",
"proc-macro2 1.0.67",
"quote 1.0.33",
"syn 1.0.109",
]
[[package]]
name = "anchor-attribute-program"
version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b18c4f191331e078d4a6a080954d1576241c29c56638783322a18d308ab27e4f"
dependencies = [
"anchor-syn 0.29.0",
"quote 1.0.33",
"syn 1.0.109",
]
[[package]]
name = "anchor-client"
version = "0.28.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8434a6bf33efba0c93157f7fa2fafac658cb26ab75396886dcedd87c2a8ad445"
dependencies = [
"anchor-lang",
"anchor-lang 0.28.0",
"anyhow",
"futures 0.3.28",
"regex",
@ -216,13 +286,37 @@ version = "0.28.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec4720d899b3686396cced9508f23dab420f1308344456ec78ef76f98fda42af"
dependencies = [
"anchor-syn",
"anchor-syn 0.28.0",
"anyhow",
"proc-macro2 1.0.67",
"quote 1.0.33",
"syn 1.0.109",
]
[[package]]
name = "anchor-derive-accounts"
version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5de10d6e9620d3bcea56c56151cad83c5992f50d5960b3a9bebc4a50390ddc3c"
dependencies = [
"anchor-syn 0.29.0",
"quote 1.0.33",
"syn 1.0.109",
]
[[package]]
name = "anchor-derive-serde"
version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4e2e5be518ec6053d90a2a7f26843dbee607583c779e6c8395951b9739bdfbe"
dependencies = [
"anchor-syn 0.29.0",
"borsh-derive-internal 0.10.3",
"proc-macro2 1.0.67",
"quote 1.0.33",
"syn 1.0.109",
]
[[package]]
name = "anchor-derive-space"
version = "0.28.0"
@ -234,20 +328,56 @@ dependencies = [
"syn 1.0.109",
]
[[package]]
name = "anchor-derive-space"
version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ecc31d19fa54840e74b7a979d44bcea49d70459de846088a1d71e87ba53c419"
dependencies = [
"proc-macro2 1.0.67",
"quote 1.0.33",
"syn 1.0.109",
]
[[package]]
name = "anchor-lang"
version = "0.28.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d2d4b20100f1310a774aba3471ef268e5c4ba4d5c28c0bbe663c2658acbc414"
dependencies = [
"anchor-attribute-access-control",
"anchor-attribute-account",
"anchor-attribute-constant",
"anchor-attribute-error",
"anchor-attribute-event",
"anchor-attribute-program",
"anchor-derive-accounts",
"anchor-derive-space",
"anchor-attribute-access-control 0.28.0",
"anchor-attribute-account 0.28.0",
"anchor-attribute-constant 0.28.0",
"anchor-attribute-error 0.28.0",
"anchor-attribute-event 0.28.0",
"anchor-attribute-program 0.28.0",
"anchor-derive-accounts 0.28.0",
"anchor-derive-space 0.28.0",
"arrayref",
"base64 0.13.1",
"bincode",
"borsh 0.10.3",
"bytemuck",
"getrandom 0.2.10",
"solana-program",
"thiserror",
]
[[package]]
name = "anchor-lang"
version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35da4785497388af0553586d55ebdc08054a8b1724720ef2749d313494f2b8ad"
dependencies = [
"anchor-attribute-access-control 0.29.0",
"anchor-attribute-account 0.29.0",
"anchor-attribute-constant 0.29.0",
"anchor-attribute-error 0.29.0",
"anchor-attribute-event 0.29.0",
"anchor-attribute-program 0.29.0",
"anchor-derive-accounts 0.29.0",
"anchor-derive-serde",
"anchor-derive-space 0.29.0",
"arrayref",
"base64 0.13.1",
"bincode",
@ -264,13 +394,27 @@ version = "0.28.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78f860599da1c2354e7234c768783049eb42e2f54509ecfc942d2e0076a2da7b"
dependencies = [
"anchor-lang",
"anchor-lang 0.28.0",
"solana-program",
"spl-associated-token-account 1.1.3",
"spl-token 3.5.0",
"spl-token-2022 0.6.1",
]
[[package]]
name = "anchor-spl"
version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c4fd6e43b2ca6220d2ef1641539e678bfc31b6cc393cf892b373b5997b6a39a"
dependencies = [
"anchor-lang 0.29.0",
"mpl-token-metadata 3.2.3",
"solana-program",
"spl-associated-token-account 2.2.0",
"spl-token 4.0.0",
"spl-token-2022 0.9.0",
]
[[package]]
name = "anchor-syn"
version = "0.28.0"
@ -289,6 +433,24 @@ dependencies = [
"thiserror",
]
[[package]]
name = "anchor-syn"
version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9101b84702fed2ea57bd22992f75065da5648017135b844283a2f6d74f27825"
dependencies = [
"anyhow",
"bs58 0.5.0",
"heck 0.3.3",
"proc-macro2 1.0.67",
"quote 1.0.33",
"serde",
"serde_json",
"sha2 0.10.7",
"syn 1.0.109",
"thiserror",
]
[[package]]
name = "android-tzdata"
version = "0.1.1"
@ -2089,19 +2251,6 @@ dependencies = [
"typenum",
]
[[package]]
name = "fixed"
version = "1.11.0"
source = "git+https://github.com/openbook-dex/openbook-v2.git#deb70f66c3294f4f8942f12f46ef40730f5d23c6"
dependencies = [
"az",
"borsh 0.9.3",
"bytemuck",
"half",
"serde",
"typenum",
]
[[package]]
name = "fixedbitset"
version = "0.4.2"
@ -3364,7 +3513,7 @@ dependencies = [
"bytemuck",
"bytes 1.5.0",
"chrono",
"fixed 1.11.0 (git+https://github.com/blockworks-foundation/fixed.git?branch=v1.11.0-borsh0_10-mango)",
"fixed",
"futures 0.3.28",
"futures-core",
"itertools",
@ -3381,10 +3530,10 @@ dependencies = [
[[package]]
name = "mango-v4"
version = "0.24.0"
version = "0.25.0"
dependencies = [
"anchor-lang",
"anchor-spl",
"anchor-lang 0.28.0",
"anchor-spl 0.28.0",
"anyhow",
"arrayref",
"async-trait",
@ -3396,7 +3545,7 @@ dependencies = [
"default-env",
"derivative",
"env_logger",
"fixed 1.11.0 (git+https://github.com/blockworks-foundation/fixed.git?branch=v1.11.0-borsh0_10-mango)",
"fixed",
"itertools",
"lazy_static",
"log 0.4.20",
@ -3427,14 +3576,14 @@ name = "mango-v4-cli"
version = "0.3.0"
dependencies = [
"anchor-client",
"anchor-lang",
"anchor-spl",
"anchor-lang 0.28.0",
"anchor-spl 0.28.0",
"anyhow",
"async-channel",
"base64 0.21.4",
"clap 3.2.25",
"dotenv",
"fixed 1.11.0 (git+https://github.com/blockworks-foundation/fixed.git?branch=v1.11.0-borsh0_10-mango)",
"fixed",
"futures 0.3.28",
"itertools",
"mango-v4",
@ -3453,8 +3602,8 @@ name = "mango-v4-client"
version = "0.3.0"
dependencies = [
"anchor-client",
"anchor-lang",
"anchor-spl",
"anchor-lang 0.28.0",
"anchor-spl 0.28.0",
"anyhow",
"async-channel",
"async-once-cell",
@ -3465,13 +3614,14 @@ dependencies = [
"borsh 0.10.3",
"clap 3.2.25",
"derive_builder",
"fixed 1.11.0 (git+https://github.com/blockworks-foundation/fixed.git?branch=v1.11.0-borsh0_10-mango)",
"fixed",
"futures 0.3.28",
"itertools",
"jsonrpc-core 18.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
"jsonrpc-core-client",
"mango-feeds-connector",
"mango-v4",
"openbook-v2",
"pyth-sdk-solana",
"reqwest",
"serde",
@ -3498,12 +3648,12 @@ name = "mango-v4-keeper"
version = "0.3.0"
dependencies = [
"anchor-client",
"anchor-lang",
"anchor-spl",
"anchor-lang 0.28.0",
"anchor-spl 0.28.0",
"anyhow",
"clap 3.2.25",
"dotenv",
"fixed 1.11.0 (git+https://github.com/blockworks-foundation/fixed.git?branch=v1.11.0-borsh0_10-mango)",
"fixed",
"futures 0.3.28",
"itertools",
"lazy_static",
@ -3524,7 +3674,7 @@ name = "mango-v4-liquidator"
version = "0.0.1"
dependencies = [
"anchor-client",
"anchor-lang",
"anchor-lang 0.28.0",
"anyhow",
"arrayref",
"async-channel",
@ -3537,7 +3687,7 @@ dependencies = [
"chrono",
"clap 3.2.25",
"dotenv",
"fixed 1.11.0 (git+https://github.com/blockworks-foundation/fixed.git?branch=v1.11.0-borsh0_10-mango)",
"fixed",
"futures 0.3.28",
"futures-core",
"futures-util",
@ -3550,6 +3700,7 @@ dependencies = [
"mango-v4",
"mango-v4-client",
"once_cell",
"openbook-v2",
"pyth-sdk-solana",
"rand 0.7.3",
"regex",
@ -3575,7 +3726,7 @@ name = "mango-v4-settler"
version = "0.0.1"
dependencies = [
"anchor-client",
"anchor-lang",
"anchor-lang 0.28.0",
"anyhow",
"arrayref",
"async-channel",
@ -3587,7 +3738,7 @@ dependencies = [
"bytes 1.5.0",
"clap 3.2.25",
"dotenv",
"fixed 1.11.0 (git+https://github.com/blockworks-foundation/fixed.git?branch=v1.11.0-borsh0_10-mango)",
"fixed",
"futures 0.3.28",
"futures-core",
"futures-util",
@ -3869,11 +4020,24 @@ dependencies = [
"num-traits",
"shank",
"solana-program",
"spl-associated-token-account 2.1.0",
"spl-associated-token-account 2.2.0",
"spl-token 4.0.0",
"thiserror",
]
[[package]]
name = "mpl-token-metadata"
version = "3.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba8ee05284d79b367ae8966d558e1a305a781fc80c9df51f37775169117ba64f"
dependencies = [
"borsh 0.10.3",
"num-derive 0.3.3",
"num-traits",
"solana-program",
"thiserror",
]
[[package]]
name = "mpl-token-metadata-context-derive"
version = "0.2.1"
@ -4265,19 +4429,21 @@ checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
[[package]]
name = "openbook-v2"
version = "0.1.0"
source = "git+https://github.com/openbook-dex/openbook-v2.git#deb70f66c3294f4f8942f12f46ef40730f5d23c6"
source = "git+https://github.com/openbook-dex/openbook-v2.git?rev=270b2d2d473862bd4e3aa213feb970af81f4b3e2#270b2d2d473862bd4e3aa213feb970af81f4b3e2"
dependencies = [
"anchor-lang",
"anchor-spl",
"anchor-lang 0.28.0",
"anchor-spl 0.28.0",
"arrayref",
"bytemuck",
"default-env",
"derivative",
"fixed 1.11.0 (git+https://github.com/openbook-dex/openbook-v2.git)",
"fixed",
"itertools",
"num_enum 0.5.11",
"pyth-sdk-solana",
"raydium-amm-v3",
"solana-program",
"solana-security-txt",
"static_assertions",
"switchboard-program",
"switchboard-v2",
@ -5255,14 +5421,15 @@ dependencies = [
[[package]]
name = "raydium-amm-v3"
version = "0.1.0"
source = "git+https://github.com/raydium-io/raydium-clmm.git#6e4639f7133a8852068d2d473c263f907b69cd4a"
source = "git+https://github.com/raydium-io/raydium-clmm.git#cc1adca3cbe5eca08571d19ebedad4c0b8ec4022"
dependencies = [
"anchor-lang",
"anchor-spl",
"anchor-lang 0.29.0",
"anchor-spl 0.29.0",
"arrayref",
"bytemuck",
"mpl-token-metadata",
"mpl-token-metadata 1.13.2",
"solana-program",
"spl-memo 4.0.0",
"uint",
]
@ -5896,7 +6063,7 @@ name = "serum_dex"
version = "0.5.10"
source = "git+https://github.com/grooviegermanikus/program.git?branch=groovie/v0.5.10-updates-expose-things#03f1b242db2a709af2601b4df445b2ea33a8d97d"
dependencies = [
"anchor-lang",
"anchor-lang 0.29.0",
"arrayref",
"bincode",
"bytemuck",
@ -5922,7 +6089,7 @@ name = "serum_dex"
version = "0.5.10"
source = "git+https://github.com/openbook-dex/program.git#c85e56deeaead43abbc33b7301058838b9c5136d"
dependencies = [
"anchor-lang",
"anchor-lang 0.29.0",
"arrayref",
"bincode",
"bytemuck",
@ -5948,7 +6115,7 @@ name = "service-mango-crank"
version = "0.1.0"
dependencies = [
"anchor-client",
"anchor-lang",
"anchor-lang 0.28.0",
"anyhow",
"async-channel",
"async-trait",
@ -5980,7 +6147,7 @@ name = "service-mango-fills"
version = "0.1.0"
dependencies = [
"anchor-client",
"anchor-lang",
"anchor-lang 0.28.0",
"anyhow",
"async-channel",
"async-trait",
@ -6025,7 +6192,7 @@ name = "service-mango-health"
version = "0.1.0"
dependencies = [
"anchor-client",
"anchor-lang",
"anchor-lang 0.28.0",
"anyhow",
"async-channel",
"async-trait",
@ -6033,7 +6200,7 @@ dependencies = [
"bs58 0.3.1",
"bytemuck",
"chrono",
"fixed 1.11.0 (git+https://github.com/blockworks-foundation/fixed.git?branch=v1.11.0-borsh0_10-mango)",
"fixed",
"futures 0.3.28",
"futures-channel",
"futures-core",
@ -6072,13 +6239,13 @@ name = "service-mango-orderbook"
version = "0.1.0"
dependencies = [
"anchor-client",
"anchor-lang",
"anchor-lang 0.28.0",
"anyhow",
"async-channel",
"async-trait",
"bs58 0.3.1",
"bytemuck",
"fixed 1.11.0 (git+https://github.com/blockworks-foundation/fixed.git?branch=v1.11.0-borsh0_10-mango)",
"fixed",
"futures-channel",
"futures-util",
"itertools",
@ -6105,12 +6272,12 @@ name = "service-mango-pnl"
version = "0.1.0"
dependencies = [
"anchor-client",
"anchor-lang",
"anchor-lang 0.28.0",
"anyhow",
"async-channel",
"async-trait",
"bs58 0.3.1",
"fixed 1.11.0 (git+https://github.com/blockworks-foundation/fixed.git?branch=v1.11.0-borsh0_10-mango)",
"fixed",
"jsonrpsee",
"log 0.4.20",
"mango-feeds-connector",
@ -7798,9 +7965,9 @@ dependencies = [
[[package]]
name = "spl-associated-token-account"
version = "2.1.0"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "477696277857a7b2c17a6f7f3095e835850ad1c0f11637b5bd2693ca777d8546"
checksum = "385e31c29981488f2820b2022d8e731aae3b02e6e18e2fd854e4c9a94dc44fc3"
dependencies = [
"assert_matches",
"borsh 0.10.3",
@ -7808,7 +7975,7 @@ dependencies = [
"num-traits",
"solana-program",
"spl-token 4.0.0",
"spl-token-2022 0.8.0",
"spl-token-2022 0.9.0",
"thiserror",
]
@ -7905,9 +8072,9 @@ dependencies = [
[[package]]
name = "spl-tlv-account-resolution"
version = "0.3.0"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7960b1e1a41e4238807fca0865e72a341b668137a3f2ddcd770d04fd1b374c96"
checksum = "062e148d3eab7b165582757453632ffeef490c02c86a48bfdb4988f63eefb3b9"
dependencies = [
"bytemuck",
"solana-program",
@ -7967,9 +8134,9 @@ dependencies = [
[[package]]
name = "spl-token-2022"
version = "0.8.0"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "84fc0c7a763c3f53fa12581d07ed324548a771bb648a1217e4f330b1d0a59331"
checksum = "e4abf34a65ba420584a0c35f3903f8d727d1f13ababbdc3f714c6b065a686e86"
dependencies = [
"arrayref",
"bytemuck",
@ -8003,9 +8170,9 @@ dependencies = [
[[package]]
name = "spl-transfer-hook-interface"
version = "0.2.0"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7489940049417ae5ce909314bead0670e2a5ea5c82d43ab96dc15c8fcbbccba"
checksum = "051d31803f873cabe71aec3c1b849f35248beae5d19a347d93a5c9cccc5d5a9b"
dependencies = [
"arrayref",
"bytemuck",
@ -8154,8 +8321,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "625e34dba0d9bcf6b1f5db5ccf1c0aa8db8329ff89c4d51715bbe4514140127a"
dependencies = [
"anchor-client",
"anchor-lang",
"anchor-spl",
"anchor-lang 0.28.0",
"anchor-spl 0.28.0",
"bincode",
"bytemuck",
"chrono",
@ -8195,8 +8362,8 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4b81886169f446e22edc18ead7addd9ebd141c39bf2286cb37943c92cd3af724"
dependencies = [
"anchor-lang",
"anchor-spl",
"anchor-lang 0.28.0",
"anchor-spl 0.28.0",
"bytemuck",
"rust_decimal",
"solana-program",

View File

@ -14,6 +14,7 @@ pyth-sdk-solana = "0.8.0"
# commit c85e56d (0.5.10 plus dependency updates)
serum_dex = { git = "https://github.com/openbook-dex/program.git", default-features=false }
mango-feeds-connector = "0.2.1"
openbook-v2 = { git = "https://github.com/openbook-dex/openbook-v2.git", rev = "270b2d2d473862bd4e3aa213feb970af81f4b3e2" }
# 1.16.7+ is required due to this: https://github.com/blockworks-foundation/mango-v4/issues/712
solana-address-lookup-table-program = "~1.16.7"

View File

@ -1,6 +1,6 @@
# syntax = docker/dockerfile:1.2
# Base image containing all binaries, deployed to ghcr.io/blockworks-foundation/mango-v4:latest
FROM lukemathwalker/cargo-chef:latest-rust-1.69-slim-bullseye as base
FROM lukemathwalker/cargo-chef:latest-rust-1.70-slim-bullseye as base
RUN apt-get update && apt-get -y install clang cmake perl libfindbin-libs-perl
WORKDIR /app

View File

@ -25,7 +25,7 @@ See DEVELOPING.md and FAQ-DEV.md
### Dependencies
- rust version 1.69.0
- rust version 1.70.0
- solana-cli 1.16.7
- anchor-cli 0.28.0
- npm 8.1.2

View File

@ -53,3 +53,4 @@ regex = "1.9.5"
hdrhistogram = "7.5.4"
indexmap = "2.0.0"
borsh = { version = "0.10.3", features = ["const-generics"] }
openbook-v2 = { workspace = true, features = ["no-entrypoint"] }

View File

@ -4,7 +4,10 @@ use std::time::Duration;
use itertools::Itertools;
use mango_v4::health::{HealthCache, HealthType};
use mango_v4::state::{MangoAccountValue, PerpMarketIndex, Side, TokenIndex, QUOTE_TOKEN_INDEX};
use mango_v4::state::{
MangoAccountValue, OpenbookV2Orders, PerpMarketIndex, Serum3Orders, Side, TokenIndex,
QUOTE_TOKEN_INDEX,
};
use mango_v4_client::{chain_data, MangoClient, PreparedInstructions};
use solana_sdk::signature::Signature;
@ -45,7 +48,12 @@ struct LiquidateHelper<'a> {
}
impl<'a> LiquidateHelper<'a> {
async fn serum3_close_orders(&self) -> anyhow::Result<Option<Signature>> {
async fn spot_close_orders(&self) -> anyhow::Result<Option<Signature>> {
enum SpotMarket {
Serum(Serum3Orders),
OpenbookV2(OpenbookV2Orders),
}
// look for any open serum orders or settleable balances
let serum_oos: anyhow::Result<Vec<_>> = self
.liqee
@ -56,39 +64,72 @@ impl<'a> LiquidateHelper<'a> {
Ok((*orders, *open_orders))
})
.try_collect();
let mut serum_force_cancels = serum_oos?
.into_iter()
.filter_map(|(orders, open_orders)| {
let can_force_cancel = open_orders.native_coin_total > 0
|| open_orders.native_pc_total > 0
|| open_orders.referrer_rebates_accrued > 0;
if can_force_cancel {
Some(orders)
} else {
None
}
let serum_force_cancels = serum_oos?.into_iter().filter_map(|(orders, open_orders)| {
let can_force_cancel = open_orders.native_coin_total > 0
|| open_orders.native_pc_total > 0
|| open_orders.referrer_rebates_accrued > 0;
if can_force_cancel {
Some(SpotMarket::Serum(orders))
} else {
None
}
});
let obv2_oos: anyhow::Result<Vec<_>> = self
.liqee
.active_openbook_v2_orders()
.map(|orders| {
let open_orders = self
.account_fetcher
.fetch::<openbook_v2::state::OpenOrdersAccount>(&orders.open_orders)?;
Ok((*orders, open_orders))
})
.try_collect();
let obv2_force_cancels = obv2_oos?.into_iter().filter_map(|(orders, open_orders)| {
let can_force_cancel = !open_orders.position.is_empty(open_orders.version);
if can_force_cancel {
Some(SpotMarket::OpenbookV2(orders))
} else {
None
}
});
let mut force_cancels = serum_force_cancels
.chain(obv2_force_cancels)
.collect::<Vec<_>>();
if serum_force_cancels.is_empty() {
if force_cancels.is_empty() {
return Ok(None);
}
serum_force_cancels.shuffle(&mut rand::thread_rng());
force_cancels.shuffle(&mut rand::thread_rng());
let mut ixs = PreparedInstructions::new();
let mut cancelled_markets = vec![];
let mut cancelled_serum3 = vec![];
let mut cancelled_openbook_v2 = vec![];
let mut tx_builder = self.client.transaction_builder().await?;
for force_cancel in serum_force_cancels {
for force_cancel in force_cancels {
let mut new_ixs = ixs.clone();
new_ixs.append(
self.client
.serum3_liq_force_cancel_orders_instruction(
(self.pubkey, self.liqee),
force_cancel.market_index,
&force_cancel.open_orders,
)
.await?,
);
let cancel_ix = match &force_cancel {
SpotMarket::Serum(orders) => {
self.client
.serum3_liq_force_cancel_orders_instruction(
(self.pubkey, self.liqee),
orders.market_index,
&orders.open_orders,
)
.await?
}
SpotMarket::OpenbookV2(orders) => {
self.client
.openbook_v2_liq_force_cancel_orders_instruction(
(self.pubkey, self.liqee),
orders.market_index,
&orders.open_orders,
)
.await?
}
};
new_ixs.append(cancel_ix);
let exceeds_cu_limit = new_ixs.cu > self.config.max_cu_per_transaction;
let exceeds_size_limit = {
@ -100,16 +141,20 @@ impl<'a> LiquidateHelper<'a> {
}
ixs = new_ixs;
cancelled_markets.push(force_cancel.market_index);
match force_cancel {
SpotMarket::Serum(orders) => cancelled_serum3.push(orders.market_index),
SpotMarket::OpenbookV2(orders) => cancelled_openbook_v2.push(orders.market_index),
}
}
tx_builder.instructions = ixs.to_instructions();
let txsig = tx_builder.send_and_confirm(&self.client.client).await?;
info!(
market_indexes = ?cancelled_markets,
market_indexes_serum3 = ?cancelled_serum3,
market_indexes_openbook_v2 = ?cancelled_openbook_v2,
%txsig,
"Force cancelled serum orders",
"Force cancelled spot orders",
);
Ok(Some(txsig))
}
@ -619,7 +664,7 @@ impl<'a> LiquidateHelper<'a> {
if let Some(txsig) = self.perp_close_orders().await? {
return Ok(Some(txsig));
}
if let Some(txsig) = self.serum3_close_orders().await? {
if let Some(txsig) = self.spot_close_orders().await? {
return Ok(Some(txsig));
}

View File

@ -20,7 +20,9 @@ done
# errors on enums that have tuple variants. This hack drops these from the idl.
perl -0777 -pi -e 's/ *{\s*"name": "NodeRef(?<nested>(?:[^{}[\]]+|\{(?&nested)\}|\[(?&nested)\])*)\},\n//g' \
target/idl/mango_v4.json target/types/mango_v4.ts;
# Also drop type only used in client and tests that somehow makes it into the idl
perl -0777 -pi -e 's/ *{\s*"name": "MangoAccountValue(?<nested>(?:[^{}[\]]+|\{(?&nested)\}|\[(?&nested)\])*)\},\n//g' \
target/idl/mango_v4.json target/types/mango_v4.ts;
# Reduce size of idl to be uploaded to chain
cp target/idl/mango_v4.json target/idl/mango_v4_no_docs.json
jq 'del(.types[]?.docs)' target/idl/mango_v4_no_docs.json \

View File

@ -47,3 +47,4 @@ bincode = "1.3.3"
tracing = { version = "0.1", features = ["log"] }
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
borsh = { version = "0.10.3", features = ["const-generics"] }
openbook-v2 = { workspace = true, features = ["no-entrypoint"] }

View File

@ -23,8 +23,8 @@ use mango_v4::accounts_ix::{
use mango_v4::accounts_zerocopy::KeyedAccountSharedData;
use mango_v4::health::HealthCache;
use mango_v4::state::{
Bank, Group, MangoAccountValue, OracleAccountInfos, PerpMarket, PerpMarketIndex,
PlaceOrderType, SelfTradeBehavior, Serum3MarketIndex, Side, TokenIndex,
Bank, Group, MangoAccountValue, OpenbookV2MarketIndex, OracleAccountInfos, PerpMarket,
PerpMarketIndex, PlaceOrderType, SelfTradeBehavior, Serum3MarketIndex, Side, TokenIndex,
};
use crate::confirm_transaction::{wait_for_transaction_confirmation, RpcConfirmTransactionConfig};
@ -1344,6 +1344,62 @@ impl MangoClient {
Ok(ix)
}
pub async fn openbook_v2_liq_force_cancel_orders_instruction(
&self,
liqee: (&Pubkey, &MangoAccountValue),
market_index: OpenbookV2MarketIndex,
open_orders: &Pubkey,
) -> anyhow::Result<PreparedInstructions> {
let openbook_v2_market = self.context.openbook_v2(market_index);
let base = self.context.token(openbook_v2_market.base_token_index);
let quote = self.context.token(openbook_v2_market.quote_token_index);
let (health_remaining_ams, health_cu) = self
.derive_health_check_remaining_account_metas(liqee.1, vec![], vec![], vec![])
.await
.unwrap();
let limit = 5;
let ix = PreparedInstructions::from_single(
Instruction {
program_id: mango_v4::id(),
accounts: {
let mut ams = anchor_lang::ToAccountMetas::to_account_metas(
&mango_v4::accounts::OpenbookV2LiqForceCancelOrders {
payer: self.owner(),
group: self.group(),
account: *liqee.0,
open_orders: *open_orders,
openbook_v2_market: openbook_v2_market.address,
openbook_v2_program: openbook_v2_market.openbook_v2_program,
openbook_v2_market_external: openbook_v2_market.market_external,
bids: openbook_v2_market.bids,
asks: openbook_v2_market.asks,
event_heap: openbook_v2_market.event_heap,
market_base_vault: openbook_v2_market.market_base_vault,
market_quote_vault: openbook_v2_market.market_quote_vault,
market_vault_signer: openbook_v2_market.market_authority,
quote_bank: quote.first_bank(),
quote_vault: quote.first_vault(),
base_bank: base.first_bank(),
base_vault: base.first_vault(),
token_program: Token::id(),
system_program: System::id(),
},
None,
);
ams.extend(health_remaining_ams.into_iter());
ams
},
data: anchor_lang::InstructionData::data(
&mango_v4::instruction::OpenbookV2LiqForceCancelOrders { limit },
),
},
self.instruction_cu(health_cu)
+ self.context.compute_estimates.cu_per_serum3_order_cancel * limit as u32,
);
Ok(ix)
}
pub async fn serum3_liq_force_cancel_orders(
&self,
liqee: (&Pubkey, &MangoAccountValue),

View File

@ -5,11 +5,12 @@ use anchor_client::ClientError;
use anchor_lang::__private::bytemuck;
use mango_v4::{
accounts_zerocopy::{KeyedAccountReader, KeyedAccountSharedData},
accounts_zerocopy::{KeyedAccountReader, KeyedAccountSharedData, LoadZeroCopy},
state::{
determine_oracle_type, load_orca_pool_state, load_raydium_pool_state,
oracle_state_unchecked, Group, MangoAccountValue, OracleAccountInfos, OracleConfig,
OracleConfigParams, OracleType, PerpMarketIndex, Serum3MarketIndex, TokenIndex, MAX_BANKS,
oracle_state_unchecked, Group, MangoAccountValue, OpenbookV2MarketIndex,
OracleAccountInfos, OracleConfig, OracleConfigParams, OracleType, PerpMarketIndex,
Serum3MarketIndex, TokenIndex, MAX_BANKS,
},
};
@ -93,6 +94,24 @@ pub struct Serum3MarketContext {
pub pc_lot_size: u64,
}
#[derive(Clone, PartialEq, Eq)]
pub struct OpenbookV2MarketContext {
pub address: Pubkey,
pub name: String,
pub openbook_v2_program: Pubkey,
pub market_external: Pubkey,
pub base_token_index: TokenIndex,
pub quote_token_index: TokenIndex,
pub bids: Pubkey,
pub asks: Pubkey,
pub event_heap: Pubkey,
pub market_base_vault: Pubkey,
pub market_quote_vault: Pubkey,
pub market_authority: Pubkey,
pub quote_lot_size: u64,
pub base_lot_size: u64,
}
#[derive(Clone, PartialEq, Eq)]
pub struct PerpMarketContext {
pub group: Pubkey,
@ -115,8 +134,10 @@ pub struct ComputeEstimates {
pub health_cu_per_token: u32,
pub health_cu_per_perp: u32,
pub health_cu_per_serum: u32,
pub health_cu_per_obv2: u32,
pub cu_per_serum3_order_match: u32,
pub cu_per_serum3_order_cancel: u32,
pub cu_per_openbook_v2_order_cancel: u32,
pub cu_per_perp_order_match: u32,
pub cu_per_perp_order_cancel: u32,
pub cu_per_oracle_fallback: u32,
@ -133,10 +154,12 @@ impl Default for ComputeEstimates {
health_cu_per_token: 5000,
health_cu_per_perp: 8000,
health_cu_per_serum: 6000,
health_cu_per_obv2: 6000,
// measured around 1.5k, see test_serum_compute
cu_per_serum3_order_match: 3_000,
// measured around 11k, see test_serum_compute
cu_per_serum3_order_cancel: 20_000,
cu_per_openbook_v2_order_cancel: 30_000,
// measured around 3.5k, see test_perp_compute
cu_per_perp_order_match: 7_000,
// measured around 3.5k, see test_perp_compute
@ -160,15 +183,18 @@ impl ComputeEstimates {
tokens: usize,
perps: usize,
serums: usize,
obv2s: usize,
fallbacks: usize,
) -> u32 {
let tokens: u32 = tokens.try_into().unwrap();
let perps: u32 = perps.try_into().unwrap();
let serums: u32 = serums.try_into().unwrap();
let obv2s: u32 = obv2s.try_into().unwrap();
let fallbacks: u32 = fallbacks.try_into().unwrap();
tokens * self.health_cu_per_token
+ perps * self.health_cu_per_perp
+ serums * self.health_cu_per_serum
+ obv2s * self.health_cu_per_obv2
+ fallbacks * self.cu_per_oracle_fallback
}
@ -177,6 +203,7 @@ impl ComputeEstimates {
account.active_token_positions().count(),
account.active_perp_positions().count(),
account.active_serum3_orders().count(),
account.active_openbook_v2_orders().count(),
num_fallbacks,
)
}
@ -191,6 +218,9 @@ pub struct MangoGroupContext {
pub serum3_markets: HashMap<Serum3MarketIndex, Serum3MarketContext>,
pub serum3_market_indexes_by_name: HashMap<String, Serum3MarketIndex>,
pub openbook_v2_markets: HashMap<OpenbookV2MarketIndex, OpenbookV2MarketContext>,
pub openbook_v2_market_indexes_by_name: HashMap<String, OpenbookV2MarketIndex>,
pub perp_markets: HashMap<PerpMarketIndex, PerpMarketContext>,
pub perp_market_indexes_by_name: HashMap<String, PerpMarketIndex>,
@ -228,6 +258,10 @@ impl MangoGroupContext {
self.token(self.serum3(market_index).quote_token_index)
}
pub fn openbook_v2(&self, market_index: OpenbookV2MarketIndex) -> &OpenbookV2MarketContext {
self.openbook_v2_markets.get(&market_index).unwrap()
}
pub fn token(&self, token_index: TokenIndex) -> &TokenContext {
self.tokens.get(&token_index).unwrap()
}
@ -344,6 +378,41 @@ impl MangoGroupContext {
})
.collect::<HashMap<_, _>>();
// openbook v2 markets
let openbook_v2_market_tuples = fetch_openbook_v2_markets(rpc, program, group).await?;
let openbook_v2_markets_external = stream::iter(openbook_v2_market_tuples.iter())
.then(|(_, s)| fetch_raw_account(rpc, s.openbook_v2_market_external))
.try_collect::<Vec<_>>()
.await?;
let openbook_v2_markets = openbook_v2_market_tuples
.iter()
.zip(openbook_v2_markets_external.iter())
.map(|((pk, s), market_external_account)| {
let market_external = market_external_account
.load::<openbook_v2::state::Market>()
.unwrap();
(
s.market_index,
OpenbookV2MarketContext {
address: *pk,
base_token_index: s.base_token_index,
quote_token_index: s.quote_token_index,
name: s.name().to_string(),
openbook_v2_program: s.openbook_v2_program,
market_external: s.openbook_v2_market_external,
bids: market_external.bids,
asks: market_external.asks,
event_heap: market_external.event_heap,
market_base_vault: market_external.market_base_vault,
market_quote_vault: market_external.market_quote_vault,
market_authority: market_external.market_authority,
quote_lot_size: market_external.quote_lot_size.try_into().unwrap(),
base_lot_size: market_external.base_lot_size.try_into().unwrap(),
},
)
})
.collect::<HashMap<_, _>>();
// perp markets
let perp_market_tuples = fetch_perp_markets(rpc, program, group).await?;
let perp_markets = perp_market_tuples
@ -379,6 +448,10 @@ impl MangoGroupContext {
.iter()
.map(|(i, s)| (s.name.clone(), *i))
.collect::<HashMap<_, _>>();
let openbook_v2_market_indexes_by_name = openbook_v2_markets
.iter()
.map(|(i, s)| (s.name.clone(), *i))
.collect::<HashMap<_, _>>();
let perp_market_indexes_by_name = perp_markets
.iter()
.map(|(i, p)| (p.name.clone(), *i))
@ -398,6 +471,8 @@ impl MangoGroupContext {
token_indexes_by_name,
serum3_markets,
serum3_market_indexes_by_name,
openbook_v2_markets,
openbook_v2_market_indexes_by_name,
perp_markets,
perp_market_indexes_by_name,
address_lookup_tables,
@ -439,6 +514,7 @@ impl MangoGroupContext {
}
let serum_oos = account.active_serum3_orders().map(|&s| s.open_orders);
let obv2_oos = account.active_openbook_v2_orders().map(|o| o.open_orders);
let perp_markets = account
.active_perp_positions()
.map(|&pa| self.perp_market_address(pa.market_index));
@ -471,6 +547,7 @@ impl MangoGroupContext {
.chain(perp_markets.map(to_account_meta))
.chain(perp_oracles.map(to_account_meta))
.chain(serum_oos.map(to_account_meta))
.chain(obv2_oos.map(to_account_meta))
.chain(fallback_oracles.into_iter().map(to_account_meta))
.collect();
@ -515,6 +592,10 @@ impl MangoGroupContext {
.active_serum3_orders()
.chain(account1.active_serum3_orders())
.map(|&s| s.open_orders);
let obv2_oos = account2
.active_openbook_v2_orders()
.chain(account1.active_openbook_v2_orders())
.map(|&s| s.open_orders);
let perp_market_indexes = account2
.active_perp_positions()
.chain(account1.active_perp_positions())
@ -553,6 +634,7 @@ impl MangoGroupContext {
.chain(perp_markets.map(to_account_meta))
.chain(perp_oracles.map(to_account_meta))
.chain(serum_oos.map(to_account_meta))
.chain(obv2_oos.map(to_account_meta))
.chain(fallback_oracles.into_iter().map(to_account_meta))
.collect();
@ -574,11 +656,13 @@ impl MangoGroupContext {
account1_token_count,
account1.active_perp_positions().count(),
account1.active_serum3_orders().count(),
account1.active_openbook_v2_orders().count(),
fallbacks_len,
) + self.compute_estimates.health_for_counts(
account2_token_count,
account2.active_perp_positions().count(),
account2.active_serum3_orders().count(),
account2.active_openbook_v2_orders().count(),
fallbacks_len,
);

View File

@ -1,6 +1,8 @@
use anchor_lang::{AccountDeserialize, Discriminator};
use futures::{stream, StreamExt};
use mango_v4::state::{Bank, MangoAccount, MangoAccountValue, MintInfo, PerpMarket, Serum3Market};
use mango_v4::state::{
Bank, MangoAccount, MangoAccountValue, MintInfo, OpenbookV2Market, PerpMarket, Serum3Market,
};
use solana_account_decoder::UiAccountEncoding;
use solana_client::nonblocking::rpc_client::RpcClient as RpcClientAsync;
@ -115,6 +117,22 @@ pub async fn fetch_serum3_markets(
.await
}
pub async fn fetch_openbook_v2_markets(
rpc: &RpcClientAsync,
program: Pubkey,
group: Pubkey,
) -> anyhow::Result<Vec<(Pubkey, OpenbookV2Market)>> {
fetch_anchor_accounts::<OpenbookV2Market>(
rpc,
program,
vec![RpcFilterType::Memcmp(Memcmp::new_raw_bytes(
8,
group.to_bytes().to_vec(),
))],
)
.await
}
pub async fn fetch_perp_markets(
rpc: &RpcClientAsync,
program: Pubkey,

View File

@ -16,6 +16,7 @@ pub async fn new(
) -> anyhow::Result<HealthCache> {
let active_token_len = account.active_token_positions().count();
let active_perp_len = account.active_perp_positions().count();
let active_serum3_len = account.active_serum3_orders().count();
let fallback_keys = context
.derive_fallback_oracle_keys(fallback_config, account_fetcher)
@ -43,6 +44,7 @@ pub async fn new(
n_perps: active_perp_len,
begin_perp: active_token_len * 2,
begin_serum3: active_token_len * 2 + active_perp_len * 2,
begin_openbook_v2: active_token_len * 2 + active_perp_len * 2 + active_serum3_len,
staleness_slot: None,
begin_fallback_oracles: metas.len(),
usdc_oracle_index: metas
@ -64,6 +66,7 @@ pub fn new_sync(
) -> anyhow::Result<HealthCache> {
let active_token_len = account.active_token_positions().count();
let active_perp_len = account.active_perp_positions().count();
let active_serum3_len = account.active_serum3_orders().count();
let (metas, _health_cu) = context.derive_health_check_remaining_account_metas(
account,
@ -88,6 +91,7 @@ pub fn new_sync(
n_perps: active_perp_len,
begin_perp: active_token_len * 2,
begin_serum3: active_token_len * 2 + active_perp_len * 2,
begin_openbook_v2: active_token_len * 2 + active_perp_len * 2 + active_serum3_len,
staleness_slot: None,
begin_fallback_oracles: metas.len(),
usdc_oracle_index: None,

View File

@ -182,6 +182,11 @@ async fn feed_snapshots(
mango_account
.active_serum3_orders()
.map(|serum3account| serum3account.open_orders)
.chain(
mango_account
.active_openbook_v2_orders()
.map(|obv2| obv2.open_orders),
)
.collect::<Vec<_>>()
})
.collect::<Vec<Pubkey>>();

View File

@ -1,3 +1,4 @@
use anchor_lang::Discriminator;
use jsonrpc_core::futures::StreamExt;
use jsonrpc_core_client::transports::ws;
@ -43,7 +44,7 @@ async fn feed_data(
with_context: Some(true),
account_config: account_info_config.clone(),
};
let open_orders_accounts_config = RpcProgramAccountsConfig {
let serum_oo_accounts_config = RpcProgramAccountsConfig {
// filter for only OpenOrders with v4 authority
filters: Some(vec![
RpcFilterType::DataSize(3228), // open orders size
@ -61,6 +62,25 @@ async fn feed_data(
with_context: Some(true),
account_config: account_info_config.clone(),
};
let obv2_oo_accounts_config = RpcProgramAccountsConfig {
// filter for only OpenOrders with the delegate as the mango group
// (the individual mango accounts are the owners)
filters: Some(vec![
RpcFilterType::DataSize(
8 + std::mem::size_of::<openbook_v2::state::OpenOrdersAccount>() as u64,
),
RpcFilterType::Memcmp(Memcmp::new_raw_bytes(
0,
openbook_v2::state::OpenOrdersAccount::DISCRIMINATOR.to_vec(),
)),
RpcFilterType::Memcmp(Memcmp::new_raw_bytes(
96,
config.open_orders_authority.to_bytes().to_vec(),
)),
]),
with_context: Some(true),
account_config: account_info_config.clone(),
};
let mut mango_sub = client
.program_subscribe(
mango_v4::id().to_string(),
@ -86,24 +106,31 @@ async fn feed_data(
);
}
let mut serum3_oo_sub_map = StreamMap::new();
let mut spot_oo_sub_map = StreamMap::new();
for serum_program in config.serum_programs.iter() {
serum3_oo_sub_map.insert(
spot_oo_sub_map.insert(
*serum_program,
client
.program_subscribe(
serum_program.to_string(),
Some(open_orders_accounts_config.clone()),
Some(serum_oo_accounts_config.clone()),
)
.map_err_anyhow()?,
);
}
spot_oo_sub_map.insert(
openbook_v2::id(),
client
.program_subscribe(openbook_v2::id().to_string(), Some(obv2_oo_accounts_config))
.map_err_anyhow()?,
);
// Make sure the serum3_oo_sub_map does not exit when there's no serum_programs
let _unused_serum_sender;
if config.serum_programs.is_empty() {
let (sender, receiver) = jsonrpc_core::futures::channel::mpsc::unbounded();
_unused_serum_sender = sender;
serum3_oo_sub_map.insert(
spot_oo_sub_map.insert(
Pubkey::default(),
jsonrpc_core_client::TypedSubscriptionStream::new(receiver, "foo"),
);
@ -132,12 +159,12 @@ async fn feed_data(
return Ok(());
}
},
message = serum3_oo_sub_map.next() => {
message = spot_oo_sub_map.next() => {
if let Some(data) = message {
let response = data.1.map_err_anyhow()?;
sender.send(Message::Account(AccountUpdate::from_rpc(response)?)).await.expect("sending must succeed");
} else {
warn!("serum stream closed");
warn!("spot oo stream closed");
return Ok(());
}
},

File diff suppressed because it is too large Load Diff

View File

@ -27,8 +27,9 @@
"lint": "eslint ./ts/client/src --ext ts --ext tsx --ext js --quiet",
"typecheck": "tsc --noEmit --pretty",
"prepublishOnly": "yarn validate && yarn build",
"validate": "yarn lint && yarn format",
"deduplicate": "npx yarn-deduplicate --list --fail",
"validate": "yarn lint && yarn format"
"prepare": "yarn build"
},
"devDependencies": {
"@solana/spl-governance": "^0.3.25",
@ -64,7 +65,8 @@
"dependencies": {
"@blockworks-foundation/mango-v4-settings": "0.14.15",
"@blockworks-foundation/mangolana": "0.0.14",
"@coral-xyz/anchor": "^0.28.1-beta.2",
"@coral-xyz/anchor": "^0.29.0",
"@openbook-dex/openbook-v2": "^0.1.2",
"@project-serum/serum": "0.13.65",
"@pythnetwork/client": "~2.14.0",
"@solana/spl-token": "0.3.7",
@ -80,7 +82,7 @@
"node-kraken-api": "^2.2.2"
},
"resolutions": {
"@coral-xyz/anchor": "^0.28.1-beta.2",
"@coral-xyz/anchor": "^0.29.0",
"**/@solana/web3.js/node-fetch": "npm:@blockworks-foundation/node-fetch@2.6.11",
"**/cross-fetch/node-fetch": "npm:@blockworks-foundation/node-fetch@2.6.11",
"**/@blockworks-foundation/mangolana/node-fetch": "npm:@blockworks-foundation/node-fetch@2.6.11"

View File

@ -1,6 +1,6 @@
[package]
name = "mango-v4"
version = "0.24.0"
version = "0.25.0"
description = "Created with Anchor"
edition = "2021"
@ -52,9 +52,7 @@ switchboard-program = "0.2"
switchboard-v2 = { package = "switchboard-solana", version = "0.28" }
openbook-v2 = { git = "https://github.com/openbook-dex/openbook-v2.git", features = [
"no-entrypoint",
] }
openbook-v2 = { workspace = true, features = ["no-entrypoint", "cpi", "enable-gpl"] }
[dev-dependencies]

View File

@ -15,7 +15,7 @@ pub struct AccountCreate<'info> {
seeds = [b"MangoAccount".as_ref(), group.key().as_ref(), owner.key().as_ref(), &account_num.to_le_bytes()],
bump,
payer = payer,
space = MangoAccount::space(token_count, serum3_count, perp_count, perp_oo_count, 0),
space = MangoAccount::space(token_count, serum3_count, perp_count, perp_oo_count, 0, 0),
)]
pub account: AccountLoader<'info, MangoAccountFixed>,
pub owner: Signer<'info>,
@ -39,7 +39,31 @@ pub struct AccountCreateV2<'info> {
seeds = [b"MangoAccount".as_ref(), group.key().as_ref(), owner.key().as_ref(), &account_num.to_le_bytes()],
bump,
payer = payer,
space = MangoAccount::space(token_count, serum3_count, perp_count, perp_oo_count, token_conditional_swap_count),
space = MangoAccount::space(token_count, serum3_count, perp_count, perp_oo_count, token_conditional_swap_count, 0),
)]
pub account: AccountLoader<'info, MangoAccountFixed>,
pub owner: Signer<'info>,
#[account(mut)]
pub payer: Signer<'info>,
pub system_program: Program<'info, System>,
}
#[derive(Accounts)]
#[instruction(account_num: u32, token_count: u8, serum3_count: u8, perp_count: u8, perp_oo_count: u8, token_conditional_swap_count: u8, openbook_v2_count: u8)]
pub struct AccountCreateV3<'info> {
#[account(
constraint = group.load()?.is_ix_enabled(IxGate::AccountCreate) @ MangoError::IxIsDisabled,
)]
pub group: AccountLoader<'info, Group>,
#[account(
init,
seeds = [b"MangoAccount".as_ref(), group.key().as_ref(), owner.key().as_ref(), &account_num.to_le_bytes()],
bump,
payer = payer,
space = MangoAccount::space(token_count, serum3_count, perp_count, perp_oo_count, token_conditional_swap_count, openbook_v2_count),
)]
pub account: AccountLoader<'info, MangoAccountFixed>,
pub owner: Signer<'info>,

View File

@ -27,7 +27,6 @@ pub use openbook_v2_deregister_market::*;
pub use openbook_v2_edit_market::*;
pub use openbook_v2_liq_force_cancel_orders::*;
pub use openbook_v2_place_order::*;
pub use openbook_v2_place_take_order::*;
pub use openbook_v2_register_market::*;
pub use openbook_v2_settle_funds::*;
pub use perp_cancel_all_orders::*;
@ -108,7 +107,6 @@ mod openbook_v2_deregister_market;
mod openbook_v2_edit_market;
mod openbook_v2_liq_force_cancel_orders;
mod openbook_v2_place_order;
mod openbook_v2_place_take_order;
mod openbook_v2_register_market;
mod openbook_v2_settle_funds;
mod perp_cancel_all_orders;

View File

@ -2,7 +2,10 @@ use anchor_lang::prelude::*;
use crate::error::*;
use crate::state::*;
use openbook_v2::{program::OpenbookV2, state::Market};
use openbook_v2::{
program::OpenbookV2,
state::{Market, OpenOrdersAccount},
};
#[derive(Accounts)]
pub struct OpenbookV2CancelOrder<'info> {
@ -21,7 +24,7 @@ pub struct OpenbookV2CancelOrder<'info> {
#[account(mut)]
/// CHECK: Validated inline by checking against the pubkey stored in the account at #2
pub open_orders: UncheckedAccount<'info>,
pub open_orders: AccountLoader<'info, OpenOrdersAccount>,
#[account(
has_one = group,

View File

@ -1,8 +1,12 @@
use anchor_lang::prelude::*;
use anchor_spl::token::Token;
use crate::error::MangoError;
use crate::state::*;
use openbook_v2::{program::OpenbookV2, state::Market};
use openbook_v2::{
program::OpenbookV2,
state::{Market, OpenOrdersIndexer},
};
#[derive(Accounts)]
pub struct OpenbookV2CloseOpenOrders<'info> {
@ -32,11 +36,27 @@ pub struct OpenbookV2CloseOpenOrders<'info> {
pub openbook_v2_market_external: AccountLoader<'info, Market>,
#[account(mut)]
/// CHECK: Will be checked against seeds and will be initiated by openbook v2
/// can't zerocopy this unfortunately
pub open_orders_indexer: Box<Account<'info, OpenOrdersIndexer>>,
#[account(mut)]
/// CHECK: Validated inline by checking against the pubkey stored in the account at #2
pub open_orders: UncheckedAccount<'info>,
pub open_orders_account: UncheckedAccount<'info>,
#[account(mut)]
/// CHECK: target for account rent needs no checks
pub sol_destination: UncheckedAccount<'info>,
// token_index is validated inline at #3
#[account(mut, has_one = group)]
pub base_bank: AccountLoader<'info, Bank>,
// token_index is validated inline at #3
#[account(mut, has_one = group)]
pub quote_bank: AccountLoader<'info, Bank>,
pub system_program: Program<'info, System>,
pub token_program: Program<'info, Token>,
}

View File

@ -4,7 +4,6 @@ use anchor_lang::prelude::*;
use openbook_v2::{program::OpenbookV2, state::Market};
#[derive(Accounts)]
#[instruction(account_num: u32)]
pub struct OpenbookV2CreateOpenOrders<'info> {
#[account(
constraint = group.load()?.is_ix_enabled(IxGate::OpenbookV2CreateOpenOrders) @ MangoError::IxIsDisabled,
@ -19,8 +18,6 @@ pub struct OpenbookV2CreateOpenOrders<'info> {
)]
pub account: AccountLoader<'info, MangoAccountFixed>,
pub authority: Signer<'info>,
#[account(
has_one = group,
has_one = openbook_v2_program,
@ -32,15 +29,14 @@ pub struct OpenbookV2CreateOpenOrders<'info> {
pub openbook_v2_market_external: AccountLoader<'info, Market>,
// initialized by this instruction via cpi to openbook_v2
#[account(
mut,
seeds = [b"OpenOrders".as_ref(), openbook_v2_market.key().as_ref(), openbook_v2_market_external.key().as_ref(), &account_num.to_le_bytes()],
bump,
seeds::program = openbook_v2_program.key(),
)]
#[account(mut)]
/// CHECK: Will be checked against seeds and will be initiated by openbook v2
pub open_orders: UncheckedAccount<'info>,
pub open_orders_indexer: UncheckedAccount<'info>,
#[account(mut)]
/// CHECK: Will be checked against seeds and will be initiated by openbook v2
pub open_orders_account: UncheckedAccount<'info>,
pub authority: Signer<'info>,
#[account(mut)]
pub payer: Signer<'info>,

View File

@ -13,9 +13,6 @@ pub struct OpenbookV2DeregisterMarket<'info> {
)]
pub group: AccountLoader<'info, Group>,
#[account(
constraint = group.load()?.admin == admin.key(),
)]
pub admin: Signer<'info>,
#[account(

View File

@ -6,8 +6,7 @@ use crate::state::*;
#[instruction(market_index: OpenbookV2MarketIndex)]
pub struct OpenbookV2EditMarket<'info> {
#[account(
constraint = group.load()?.openbook_v2_supported(),
constraint = group.load()?.admin == admin.key(),
has_one = admin,
)]
pub group: AccountLoader<'info, Group>,

View File

@ -1,5 +1,6 @@
use anchor_lang::prelude::*;
use anchor_spl::token::{Token, TokenAccount};
use openbook_v2::state::OpenOrdersAccount;
use crate::error::*;
use crate::state::*;
@ -19,9 +20,12 @@ pub struct OpenbookV2LiqForceCancelOrders<'info> {
)]
pub account: AccountLoader<'info, MangoAccountFixed>,
#[account(mut)]
pub payer: Signer<'info>,
#[account(mut)]
/// CHECK: Validated inline by checking against the pubkey stored in the account at #2
pub open_orders: UncheckedAccount<'info>,
pub open_orders: AccountLoader<'info, OpenOrdersAccount>,
#[account(
has_one = group,
@ -33,9 +37,12 @@ pub struct OpenbookV2LiqForceCancelOrders<'info> {
pub openbook_v2_program: Program<'info, OpenbookV2>,
#[account(
mut,
has_one = bids,
has_one = asks,
has_one = event_heap,
has_one = market_base_vault,
has_one = market_quote_vault,
)]
pub openbook_v2_market_external: AccountLoader<'info, Market>,
@ -71,4 +78,5 @@ pub struct OpenbookV2LiqForceCancelOrders<'info> {
pub base_vault: Box<Account<'info, TokenAccount>>,
pub token_program: Program<'info, Token>,
pub system_program: Program<'info, System>,
}

View File

@ -2,7 +2,74 @@ use crate::error::*;
use crate::state::*;
use anchor_lang::prelude::*;
use anchor_spl::token::{Token, TokenAccount};
use openbook_v2::{program::OpenbookV2, state::Market};
use num_enum::IntoPrimitive;
use num_enum::TryFromPrimitive;
use openbook_v2::{
program::OpenbookV2,
state::{BookSide, Market, OpenOrdersAccount, PostOrderType, SelfTradeBehavior, Side},
};
#[derive(Copy, Clone, TryFromPrimitive, IntoPrimitive, AnchorSerialize, AnchorDeserialize)]
#[repr(u8)]
pub enum OpenbookV2PlaceOrderType {
Limit = 0,
ImmediateOrCancel = 1,
PostOnly = 2,
Market = 3,
PostOnlySlide = 4,
}
impl OpenbookV2PlaceOrderType {
pub fn to_external_post_order_type(&self) -> Result<PostOrderType> {
match *self {
Self::Market => Err(MangoError::SomeError.into()),
Self::ImmediateOrCancel => Err(MangoError::SomeError.into()),
Self::Limit => Ok(PostOrderType::Limit),
Self::PostOnly => Ok(PostOrderType::PostOnly),
Self::PostOnlySlide => Ok(PostOrderType::PostOnlySlide),
}
}
}
#[derive(Copy, Clone, TryFromPrimitive, IntoPrimitive, AnchorSerialize, AnchorDeserialize)]
#[repr(u8)]
pub enum OpenbookV2PostOrderType {
Limit = 0,
PostOnly = 2,
PostOnlySlide = 4,
}
#[derive(Copy, Clone, TryFromPrimitive, IntoPrimitive, AnchorSerialize, AnchorDeserialize)]
#[repr(u8)]
pub enum OpenbookV2SelfTradeBehavior {
DecrementTake = 0,
CancelProvide = 1,
AbortTransaction = 2,
}
impl OpenbookV2SelfTradeBehavior {
pub fn to_external(&self) -> SelfTradeBehavior {
match *self {
OpenbookV2SelfTradeBehavior::DecrementTake => SelfTradeBehavior::DecrementTake,
OpenbookV2SelfTradeBehavior::CancelProvide => SelfTradeBehavior::CancelProvide,
OpenbookV2SelfTradeBehavior::AbortTransaction => SelfTradeBehavior::AbortTransaction,
}
}
}
#[derive(Copy, Clone, TryFromPrimitive, IntoPrimitive, AnchorSerialize, AnchorDeserialize)]
#[repr(u8)]
pub enum OpenbookV2Side {
Bid = 0,
Ask = 1,
}
impl OpenbookV2Side {
pub fn to_external(&self) -> Side {
match *self {
Self::Bid => Side::Bid,
Self::Ask => Side::Ask,
}
}
}
#[derive(Accounts)]
pub struct OpenbookV2PlaceOrder<'info> {
@ -22,9 +89,13 @@ pub struct OpenbookV2PlaceOrder<'info> {
pub authority: Signer<'info>,
#[account(mut)]
/// CHECK: Validated inline by checking against the pubkey stored in the account at #2
pub open_orders: UncheckedAccount<'info>,
pub open_orders: AccountLoader<'info, OpenOrdersAccount>,
#[account(
has_one = group,
has_one = openbook_v2_market_external,
has_one = openbook_v2_program,
)]
pub openbook_v2_market: AccountLoader<'info, OpenbookV2Market>,
pub openbook_v2_program: Program<'info, OpenbookV2>,
@ -39,38 +110,36 @@ pub struct OpenbookV2PlaceOrder<'info> {
#[account(mut)]
/// CHECK: bids will be checked by openbook_v2
pub bids: UncheckedAccount<'info>,
pub bids: AccountLoader<'info, BookSide>,
#[account(mut)]
/// CHECK: asks will be checked by openbook_v2
pub asks: UncheckedAccount<'info>,
pub asks: AccountLoader<'info, BookSide>,
#[account(mut)]
/// CHECK: event queue will be checked by openbook_v2
pub event_heap: UncheckedAccount<'info>,
#[account(mut)]
/// CHECK: base vault will be checked by openbook_v2
pub market_base_vault: Box<Account<'info, TokenAccount>>,
#[account(mut)]
/// CHECK: quote vault will be checked by openbook_v2
pub market_quote_vault: Box<Account<'info, TokenAccount>>,
/// CHECK: vault will be checked by openbook_v2
pub market_vault: Box<Account<'info, TokenAccount>>,
/// CHECK: Validated by the openbook_v2 cpi call
pub market_vault_signer: UncheckedAccount<'info>,
/// The bank that pays for the order, if necessary
// token_index and payer_bank.vault == payer_vault is validated inline at #3
/// The bank that pays for the order. Bank oracle also expected in remaining_accounts
// payer_bank.vault == payer_vault is validated inline at #3
// bank.token_index is validated against the openbook market at #4
#[account(mut, has_one = group)]
pub payer_bank: AccountLoader<'info, Bank>,
/// The bank vault that pays for the order, if necessary
/// The bank vault that pays for the order
#[account(mut)]
pub payer_vault: Box<Account<'info, TokenAccount>>,
/// CHECK: The oracle can be one of several different account types
#[account(address = payer_bank.load()?.oracle)]
pub payer_oracle: UncheckedAccount<'info>,
/// The bank that receives the funds upon settlement. Bank oracle also expected in remaining_accounts
// bank.token_index is validated against the openbook market at #4
#[account(mut, has_one = group)]
pub receiver_bank: AccountLoader<'info, Bank>,
pub token_program: Program<'info, Token>,
}

View File

@ -1,85 +0,0 @@
use crate::error::*;
use crate::state::*;
use anchor_lang::prelude::*;
use anchor_spl::token::{Token, TokenAccount};
use openbook_v2::{program::OpenbookV2, state::Market};
#[derive(Accounts)]
pub struct OpenbookV2PlaceTakeOrder<'info> {
#[account(
constraint = group.load()?.is_ix_enabled(IxGate::OpenbookV2PlaceTakeOrder) @ MangoError::IxIsDisabled,
)]
pub group: AccountLoader<'info, Group>,
#[account(
mut,
has_one = group,
constraint = account.load()?.is_operational() @ MangoError::AccountIsFrozen
// authority is checked at #1
)]
pub account: AccountLoader<'info, MangoAccountFixed>,
pub authority: Signer<'info>,
#[account(
has_one = group,
has_one = openbook_v2_program,
has_one = openbook_v2_market_external,
)]
pub openbook_v2_market: AccountLoader<'info, OpenbookV2Market>,
pub openbook_v2_program: Program<'info, OpenbookV2>,
#[account(
mut,
has_one = bids,
has_one = asks,
has_one = event_heap,
)]
pub openbook_v2_market_external: AccountLoader<'info, Market>,
/// CHECK: Validated by the openbook_v2 cpi call
#[account(mut)]
pub bids: UncheckedAccount<'info>,
#[account(mut)]
/// CHECK: Validated by the openbook_v2 cpi call
pub asks: UncheckedAccount<'info>,
#[account(mut)]
/// CHECK: Validated by the openbook_v2 cpi call
pub event_heap: UncheckedAccount<'info>,
#[account(mut)]
/// CHECK: Validated by the openbook_v2 cpi call
pub market_request_queue: UncheckedAccount<'info>,
#[account(
mut,
constraint = market_base_vault.mint == payer_vault.mint,
)]
/// CHECK: Validated by the openbook_v2 cpi call
pub market_base_vault: Box<Account<'info, TokenAccount>>,
#[account(mut)]
/// CHECK: Validated by the openbook_v2 cpi call
pub market_quote_vault: Box<Account<'info, TokenAccount>>,
/// CHECK: Validated by the openbook_v2 cpi call
pub market_vault_signer: UncheckedAccount<'info>,
/// The bank that pays for the order, if necessary
// token_index and payer_bank.vault == payer_vault is validated inline at #3
#[account(mut, has_one = group)]
pub payer_bank: AccountLoader<'info, Bank>,
/// The bank vault that pays for the order, if necessary
#[account(mut)]
pub payer_vault: Box<Account<'info, TokenAccount>>,
/// CHECK: The oracle can be one of several different account types
#[account(address = payer_bank.load()?.oracle)]
pub payer_oracle: UncheckedAccount<'info>,
pub token_program: Program<'info, Token>,
}

View File

@ -8,20 +8,20 @@ use openbook_v2::{program::OpenbookV2, state::Market};
pub struct OpenbookV2RegisterMarket<'info> {
#[account(
mut,
has_one = admin,
constraint = group.load()?.is_ix_enabled(IxGate::OpenbookV2RegisterMarket) @ MangoError::IxIsDisabled,
constraint = group.load()?.openbook_v2_supported()
)]
pub group: AccountLoader<'info, Group>,
/// group admin or fast listing admin, checked at #1
pub admin: Signer<'info>,
/// CHECK: Can register a market for any openbook_v2 program
pub openbook_v2_program: Program<'info, OpenbookV2>,
#[account(
constraint = openbook_v2_market_external.load()?.base_mint == base_bank.load()?.mint,
constraint = openbook_v2_market_external.load()?.quote_mint == quote_bank.load()?.mint,
constraint = openbook_v2_market_external.load()?.close_market_admin.is_none(),
constraint = openbook_v2_market_external.load()?.open_orders_admin.is_none(),
constraint = openbook_v2_market_external.load()?.consume_events_admin.is_none(),
)]
pub openbook_v2_market_external: AccountLoader<'info, Market>,

View File

@ -1,5 +1,6 @@
use anchor_lang::prelude::*;
use anchor_spl::token::{Token, TokenAccount};
use openbook_v2::state::OpenOrdersAccount;
use crate::error::*;
use crate::state::*;
@ -20,11 +21,11 @@ pub struct OpenbookV2SettleFunds<'info> {
)]
pub account: AccountLoader<'info, MangoAccountFixed>,
#[account(mut)]
pub authority: Signer<'info>,
#[account(mut)]
/// CHECK: Validated inline by checking against the pubkey stored in the account at #2
pub open_orders: UncheckedAccount<'info>,
pub open_orders: AccountLoader<'info, OpenOrdersAccount>,
#[account(
has_one = group,
@ -35,19 +36,17 @@ pub struct OpenbookV2SettleFunds<'info> {
pub openbook_v2_program: Program<'info, OpenbookV2>,
#[account(mut)]
#[account(
mut,
has_one = market_base_vault,
has_one = market_quote_vault,
)]
pub openbook_v2_market_external: AccountLoader<'info, Market>,
#[account(
mut,
constraint = market_base_vault.mint == base_vault.mint,
)]
#[account(mut)]
pub market_base_vault: Box<Account<'info, TokenAccount>>,
#[account(
mut,
constraint = market_quote_vault.mint == quote_vault.mint,
)]
#[account(mut)]
pub market_quote_vault: Box<Account<'info, TokenAccount>>,
/// needed for the automatic settle_funds call
@ -67,10 +66,11 @@ pub struct OpenbookV2SettleFunds<'info> {
#[account(mut)]
pub base_vault: Box<Account<'info, TokenAccount>>,
/// CHECK: The oracle can be one of several different account types and the pubkey is checked in the parent
/// CHECK: validated against banks at #4
pub quote_oracle: UncheckedAccount<'info>,
/// CHECK: The oracle can be one of several different account types and the pubkey is checked in the parent
/// CHECK: validated against banks at #4
pub base_oracle: UncheckedAccount<'info>,
pub token_program: Program<'info, Token>,
pub system_program: Program<'info, System>,
}

View File

@ -73,8 +73,8 @@ pub enum MangoError {
GroupIsHalted,
#[msg("the perp position has non-zero base lots")]
PerpHasBaseLots,
#[msg("there are open or unsettled serum3 orders")]
HasOpenOrUnsettledSerum3Orders,
#[msg("there are open or unsettled spot orders")]
HasOpenOrUnsettledSpotOrders,
#[msg("has liquidatable token position")]
HasLiquidatableTokenPosition,
#[msg("has liquidatable perp base position")]
@ -128,7 +128,7 @@ pub enum MangoError {
#[msg("a bank in the health account list should be writable but is not")]
HealthAccountBankNotWritable,
#[msg("the market does not allow limit orders too far from the current oracle value")]
Serum3PriceBandExceeded,
SpotPriceBandExceeded,
#[msg("deposit crosses the token's deposit limit")]
BankDepositLimit,
#[msg("delegates can only withdraw to the owner's associated token account")]
@ -151,6 +151,10 @@ pub enum MangoError {
InvalidSequenceNumber,
#[msg("invalid health")]
InvalidHealth,
#[msg("no free openbook v2 open orders index")]
NoFreeOpenbookV2OpenOrdersIndex,
#[msg("openbook v2 open orders exist already")]
OpenbookV2OpenOrdersExistAlready,
}
impl MangoError {

View File

@ -1,7 +1,9 @@
use anchor_lang::prelude::*;
use anchor_lang::Discriminator;
use anchor_lang::ZeroCopy;
use fixed::types::I80F48;
use openbook_v2::state::OpenOrdersAccount;
use serum_dex::state::OpenOrders;
use std::cell::Ref;
@ -37,6 +39,11 @@ pub trait AccountRetriever {
) -> Result<(&Bank, I80F48)>;
fn serum_oo(&self, active_serum_oo_index: usize, key: &Pubkey) -> Result<&OpenOrders>;
fn openbook_oo(
&self,
active_openbook_oo_index: usize,
key: &Pubkey,
) -> Result<&OpenOrdersAccount>;
fn perp_market_and_oracle_price(
&self,
@ -61,6 +68,7 @@ pub struct FixedOrderAccountRetriever<T: KeyedAccountReader> {
pub n_perps: usize,
pub begin_perp: usize,
pub begin_serum3: usize,
pub begin_openbook_v2: usize,
pub staleness_slot: Option<u64>,
pub begin_fallback_oracles: usize,
pub usdc_oracle_index: Option<usize>,
@ -120,14 +128,16 @@ pub fn new_fixed_order_account_retriever_inner<'a, 'info>(
n_banks: usize,
) -> Result<FixedOrderAccountRetriever<AccountInfoRef<'a, 'info>>> {
let active_serum3_len = account.active_serum3_orders().count();
let active_openbook_v2_len = account.active_openbook_v2_orders().count();
let active_perp_len = account.active_perp_positions().count();
let expected_ais = n_banks * 2 // banks + oracles
+ active_perp_len * 2 // PerpMarkets + Oracles
+ active_serum3_len; // open_orders
+ active_serum3_len // open_orders
+ active_openbook_v2_len; // open_orders
require_msg_typed!(ais.len() >= expected_ais, MangoError::InvalidHealthAccountCount,
"received {} accounts but expected {} ({} banks, {} bank oracles, {} perp markets, {} perp oracles, {} serum3 oos)",
"received {} accounts but expected {} ({} banks, {} bank oracles, {} perp markets, {} perp oracles, {} serum3 oos, {} obv2 oos)",
ais.len(), expected_ais,
n_banks, n_banks, active_perp_len, active_perp_len, active_serum3_len
n_banks, n_banks, active_perp_len, active_perp_len, active_serum3_len, active_openbook_v2_len
);
let usdc_oracle_index = ais[..]
.iter()
@ -142,6 +152,7 @@ pub fn new_fixed_order_account_retriever_inner<'a, 'info>(
n_perps: active_perp_len,
begin_perp: n_banks * 2,
begin_serum3: n_banks * 2 + active_perp_len * 2,
begin_openbook_v2: n_banks * 2 + active_perp_len * 2 + active_serum3_len,
staleness_slot: Some(now_slot),
begin_fallback_oracles: expected_ais,
usdc_oracle_index,
@ -292,6 +303,27 @@ impl<T: KeyedAccountReader> AccountRetriever for FixedOrderAccountRetriever<T> {
)
})
}
fn openbook_oo(
&self,
active_openbook_oo_index: usize,
key: &Pubkey,
) -> Result<&OpenOrdersAccount> {
let openbook_oo_index = self.begin_openbook_v2 + active_openbook_oo_index;
let ai = &self.ais[openbook_oo_index];
(|| {
require_keys_eq!(*key, *ai.key());
let loaded = ai.load::<OpenOrdersAccount>()?;
Ok(loaded)
})()
.with_context(|| {
format!(
"loading openbook open orders with health account index {}, passed account {}",
openbook_oo_index,
ai.key(),
)
})
}
}
pub struct ScannedBanksAndOracles<'a, 'info> {
@ -404,6 +436,7 @@ impl<'a, 'info> ScannedBanksAndOracles<'a, 'info> {
/// - an unknown number of PerpMarket accounts
/// - the same number of oracles in the same order as the perp markets
/// - an unknown number of serum3 OpenOrders accounts
/// - an unknown number of openbook_v2 OpenOrders accounts
/// - an unknown number of fallback oracle accounts
/// and retrieves accounts needed for the health computation by doing a linear
/// scan for each request.
@ -411,7 +444,7 @@ pub struct ScanningAccountRetriever<'a, 'info> {
banks_and_oracles: ScannedBanksAndOracles<'a, 'info>,
perp_markets: Vec<AccountInfoRef<'a, 'info>>,
perp_oracles: Vec<AccountInfoRef<'a, 'info>>,
serum3_oos: Vec<AccountInfoRef<'a, 'info>>,
spot_oos: Vec<AccountInfoRef<'a, 'info>>,
perp_index_map: HashMap<PerpMarketIndex, usize>,
}
@ -497,7 +530,16 @@ impl<'a, 'info> ScanningAccountRetriever<'a, 'info> {
&& serum3_cpi::has_serum_header(&x.data.borrow())
})
.count();
let fallback_oracles_start = serum3_start + n_serum3;
let openbook_v2_start = serum3_start + n_serum3;
let n_openbook_v2 = ais[openbook_v2_start..]
.iter()
.take_while(|x| {
x.data_len() == std::mem::size_of::<openbook_v2::state::OpenOrdersAccount>() + 8
&& x.data.borrow()[0..8]
== openbook_v2::state::OpenOrdersAccount::discriminator()
})
.count();
let fallback_oracles_start = openbook_v2_start + n_openbook_v2;
let usd_oracle_index = ais[fallback_oracles_start..]
.iter()
.position(|o| o.key == &pyth_mainnet_usdc_oracle::ID);
@ -517,7 +559,7 @@ impl<'a, 'info> ScanningAccountRetriever<'a, 'info> {
},
perp_markets: AccountInfoRef::borrow_slice(&ais[perps_start..perp_oracles_start])?,
perp_oracles: AccountInfoRef::borrow_slice(&ais[perp_oracles_start..serum3_start])?,
serum3_oos: AccountInfoRef::borrow_slice(&ais[serum3_start..fallback_oracles_start])?,
spot_oos: AccountInfoRef::borrow_slice(&ais[serum3_start..fallback_oracles_start])?,
perp_index_map,
})
}
@ -560,13 +602,23 @@ impl<'a, 'info> ScanningAccountRetriever<'a, 'info> {
pub fn scanned_serum_oo(&self, key: &Pubkey) -> Result<&OpenOrders> {
let oo = self
.serum3_oos
.spot_oos
.iter()
.find(|ai| ai.key == key)
.ok_or_else(|| error_msg!("no serum3 open orders for key {}", key))?;
serum3_cpi::load_open_orders(oo)
}
pub fn scanned_openbook_oo(&self, key: &Pubkey) -> Result<&OpenOrdersAccount> {
let oo = self
.spot_oos
.iter()
.find(|ai| ai.key == key)
.ok_or_else(|| error_msg!("no openbook open orders for key {}", key))?;
let loaded = oo.load::<OpenOrdersAccount>()?;
Ok(loaded)
}
pub fn into_banks_and_oracles(self) -> ScannedBanksAndOracles<'a, 'info> {
self.banks_and_oracles
}
@ -598,6 +650,10 @@ impl<'a, 'info> AccountRetriever for ScanningAccountRetriever<'a, 'info> {
fn serum_oo(&self, _account_index: usize, key: &Pubkey) -> Result<&OpenOrders> {
self.scanned_serum_oo(key)
}
fn openbook_oo(&self, _account_index: usize, key: &Pubkey) -> Result<&OpenOrdersAccount> {
self.scanned_openbook_oo(key)
}
}
#[cfg(test)]
@ -606,6 +662,7 @@ mod tests {
use super::super::test::*;
use super::*;
use openbook_v2::state::OpenOrdersAccount;
use serum_dex::state::OpenOrders;
use std::convert::identity;
@ -626,6 +683,10 @@ mod tests {
let oo1key = oo1.pubkey;
oo1.data().native_pc_total = 20;
let mut oo2 = TestAccount::<OpenOrdersAccount>::new_zeroed();
let oo2key = oo2.pubkey;
oo2.data().position.asks_base_lots = 21;
let mut perp1 = mock_perp_market(
group,
oracle2.pubkey,
@ -657,6 +718,7 @@ mod tests {
oracle2_account_info,
oracle1_account_info,
oo1.as_account_info(),
oo2.as_account_info(),
];
let mut retriever =
@ -668,7 +730,7 @@ mod tests {
assert_eq!(retriever.perp_markets.len(), 2);
assert_eq!(retriever.perp_oracles.len(), 2);
assert_eq!(retriever.perp_index_map.len(), 2);
assert_eq!(retriever.serum3_oos.len(), 1);
assert_eq!(retriever.spot_oos.len(), 2);
{
let (b1, o1, opt_b2o2) = retriever.banks_mut_and_oracles(1, 4).unwrap();
@ -703,11 +765,23 @@ mod tests {
assert_eq!(o, 5 * I80F48::ONE);
}
let oo = retriever.serum_oo(0, &oo1key).unwrap();
assert_eq!(identity(oo.native_pc_total), 20);
let oo1 = retriever.serum_oo(0, &oo1key).unwrap();
assert_eq!(identity(oo1.native_pc_total), 20);
assert!(retriever.serum_oo(1, &Pubkey::default()).is_err());
let oo2 = retriever.openbook_oo(0, &oo2key).unwrap();
assert_eq!(identity(oo2.position.asks_base_lots), 21);
assert!(retriever.openbook_oo(1, &Pubkey::default()).is_err());
// check retrieval fails when using the wrong function for the account type
retriever
.serum_oo(0, &oo2key)
.map(|_| "should fail to load serum3 oo")
.unwrap_err();
retriever.openbook_oo(0, &oo1key).unwrap_err();
let (perp, oracle_price) = retriever
.perp_market_and_oracle_price(&group, 0, 9)
.unwrap();

View File

@ -17,13 +17,14 @@
use anchor_lang::prelude::*;
use fixed::types::I80F48;
use openbook_v2::state::OpenOrdersAccount;
use crate::error::*;
use crate::i80f48::LowPrecisionDivision;
use crate::serum3_cpi::{OpenOrdersAmounts, OpenOrdersSlim};
use crate::state::{
Bank, MangoAccountRef, PerpMarket, PerpMarketIndex, PerpPosition, Serum3MarketIndex,
Serum3Orders, TokenIndex,
Bank, MangoAccountRef, OpenbookV2MarketIndex, OpenbookV2Orders, PerpMarket, PerpMarketIndex,
PerpPosition, Serum3MarketIndex, Serum3Orders, TokenIndex,
};
use super::*;
@ -188,8 +189,8 @@ pub struct TokenBalance {
#[derive(Clone, Default)]
pub struct TokenMaxReserved {
/// The sum of serum-reserved amounts over all markets
pub max_serum_reserved: I80F48,
/// The sum of reserved amounts over all serum3 and openbookV2 markets
pub max_spot_reserved: I80F48,
}
impl TokenInfo {
@ -232,14 +233,20 @@ impl TokenInfo {
}
}
/// Information about reserved funds on Serum3 open orders accounts.
#[derive(Clone, Debug, PartialEq)]
pub enum SpotMarketIndex {
Serum3(Serum3MarketIndex),
OpenbookV2(OpenbookV2MarketIndex),
}
/// Information about reserved funds on Serum3 and Openbook V2 open orders accounts.
///
/// Note that all "free" funds on open orders accounts are added directly
/// to the token info. This is only about dealing with the reserved funds
/// that might end up as base OR quote tokens, depending on whether the
/// open orders execute on not.
#[derive(Clone, Debug)]
pub struct Serum3Info {
pub struct SpotInfo {
// reserved amounts as stored on the open orders
pub reserved_base: I80F48,
pub reserved_quote: I80F48,
@ -253,14 +260,14 @@ pub struct Serum3Info {
pub base_info_index: usize,
pub quote_info_index: usize,
pub market_index: Serum3MarketIndex,
pub spot_market_index: SpotMarketIndex,
/// The open orders account has no free or reserved funds
pub has_zero_funds: bool,
}
impl Serum3Info {
fn new(
impl SpotInfo {
fn new_from_serum(
serum_account: &Serum3Orders,
open_orders: &impl OpenOrdersAmounts,
base_info_index: usize,
@ -282,13 +289,44 @@ impl Serum3Info {
reserved_quote_as_base_highest_bid,
base_info_index,
quote_info_index,
market_index: serum_account.market_index,
spot_market_index: SpotMarketIndex::Serum3(serum_account.market_index),
has_zero_funds: open_orders.native_base_total() == 0
&& open_orders.native_quote_total() == 0
&& open_orders.native_rebates() == 0,
}
}
fn new_from_openbook(
open_orders_account: &OpenOrdersAccount,
open_orders: &OpenbookV2Orders,
base_info_index: usize,
quote_info_index: usize,
) -> Self {
// track the reserved amounts
let reserved_base =
I80F48::from(open_orders_account.position.asks_base_lots * open_orders.base_lot_size);
let reserved_quote =
I80F48::from(open_orders_account.position.bids_quote_lots * open_orders.quote_lot_size);
let reserved_base_as_quote_lowest_ask =
reserved_base * I80F48::from_num(open_orders.lowest_placed_ask);
let reserved_quote_as_base_highest_bid =
reserved_quote * I80F48::from_num(open_orders.highest_placed_bid_inv);
Self {
reserved_base,
reserved_quote,
reserved_base_as_quote_lowest_ask,
reserved_quote_as_base_highest_bid,
base_info_index,
quote_info_index,
spot_market_index: SpotMarketIndex::OpenbookV2(open_orders.market_index),
has_zero_funds: open_orders_account
.position
.is_empty(open_orders_account.version),
}
}
#[inline(always)]
fn all_reserved_as_base(
&self,
@ -360,7 +398,7 @@ impl Serum3Info {
token_infos: &[TokenInfo],
token_balances: &[TokenBalance],
token_max_reserved: &[TokenMaxReserved],
market_reserved: &Serum3Reserved,
market_reserved: &SpotReserved,
) -> I80F48 {
if market_reserved.all_reserved_as_base.is_zero()
|| market_reserved.all_reserved_as_quote.is_zero()
@ -378,8 +416,8 @@ impl Serum3Info {
max_reserved: &TokenMaxReserved,
market_reserved: I80F48| {
// This balance includes all possible reserved funds from markets that relate to the
// token, including this market itself: `market_reserved` is already included in `max_serum_reserved`.
let max_balance = balance.spot_and_perp + max_reserved.max_serum_reserved;
// token, including this market itself: `market_reserved` is already included in `max_spot_reserved`.
let max_balance = balance.spot_and_perp + max_reserved.max_spot_reserved;
// For simplicity, we assume that `market_reserved` was added to `max_balance` last
// (it underestimates health because that gives the smallest effects): how much did
@ -416,8 +454,8 @@ impl Serum3Info {
}
#[derive(Clone)]
pub(crate) struct Serum3Reserved {
/// base tokens when the serum3info.reserved_quote get converted to base and added to reserved_base
pub(crate) struct SpotReserved {
/// base tokens when the spotinfo.reserved_quote get converted to base and added to reserved_base
all_reserved_as_base: I80F48,
/// ditto the other way around
all_reserved_as_quote: I80F48,
@ -593,7 +631,7 @@ impl PerpInfo {
#[derive(Clone, Debug)]
pub struct HealthCache {
pub token_infos: Vec<TokenInfo>,
pub(crate) serum3_infos: Vec<Serum3Info>,
pub(crate) spot_infos: Vec<SpotInfo>,
pub(crate) perp_infos: Vec<PerpInfo>,
#[allow(unused)]
pub(crate) being_liquidated: bool,
@ -641,7 +679,7 @@ impl HealthCache {
self.health_assets_and_liabs(health_type, false)
}
/// Loop over the token, perp, serum contributions and add up all positive values into `assets`
/// Loop over the token, perp, spot contributions and add up all positive values into `assets`
/// and (the abs) of negative values separately into `liabs`. Return (assets, liabs).
///
/// Due to the way token and perp positions sum before being weighted, there's some flexibility
@ -728,9 +766,9 @@ impl HealthCache {
}
let token_balances = self.effective_token_balances(health_type);
let (token_max_reserved, serum3_reserved) = self.compute_serum3_reservations(health_type);
for (serum3_info, reserved) in self.serum3_infos.iter().zip(serum3_reserved.iter()) {
let contrib = serum3_info.health_contribution(
let (token_max_reserved, spot_reserved) = self.compute_spot_reservations(health_type);
for (spot_info, reserved) in self.spot_infos.iter().zip(spot_reserved.iter()) {
let contrib = spot_info.health_contribution(
health_type,
&self.token_infos,
&token_balances,
@ -761,11 +799,11 @@ impl HealthCache {
}
}
for serum_info in self.serum3_infos.iter() {
let quote = &self.token_infos[serum_info.quote_info_index];
let base = &self.token_infos[serum_info.base_info_index];
assets += serum_info.reserved_base * base.prices.oracle;
assets += serum_info.reserved_quote * quote.prices.oracle;
for spot_info in self.spot_infos.iter() {
let quote = &self.token_infos[spot_info.quote_info_index];
let base = &self.token_infos[spot_info.base_info_index];
assets += spot_info.reserved_base * base.prices.oracle;
assets += spot_info.reserved_quote * quote.prices.oracle;
}
for perp_info in self.perp_infos.iter() {
@ -874,28 +912,71 @@ impl HealthCache {
free_base_change: I80F48,
free_quote_change: I80F48,
) -> Result<()> {
let serum_info_index = self
.serum3_infos
let spot_info_index = self
.spot_infos
.iter_mut()
.position(|m| m.market_index == serum_account.market_index)
.position(|m| {
m.spot_market_index == SpotMarketIndex::Serum3(serum_account.market_index)
})
.ok_or_else(|| error_msg!("serum3 market {} not found", serum_account.market_index))?;
let serum_info = &self.serum3_infos[serum_info_index];
let spot_info = &self.spot_infos[spot_info_index];
{
let base_entry = &mut self.token_infos[serum_info.base_info_index];
let base_entry = &mut self.token_infos[spot_info.base_info_index];
base_entry.balance_spot += free_base_change;
}
{
let quote_entry = &mut self.token_infos[serum_info.quote_info_index];
let quote_entry = &mut self.token_infos[spot_info.quote_info_index];
quote_entry.balance_spot += free_quote_change;
}
let serum_info = &mut self.serum3_infos[serum_info_index];
*serum_info = Serum3Info::new(
let spot_info = &mut self.spot_infos[spot_info_index];
*spot_info = SpotInfo::new_from_serum(
serum_account,
open_orders,
serum_info.base_info_index,
serum_info.quote_info_index,
spot_info.base_info_index,
spot_info.quote_info_index,
);
Ok(())
}
/// Recompute the cached information about a serum market.
///
/// WARNING: You must also call recompute_token_weights() after all bank
/// deposit/withdraw changes!
pub fn recompute_openbook_v2_info(
&mut self,
open_orders: &OpenbookV2Orders,
open_orders_account: &OpenOrdersAccount,
free_base_change: I80F48,
free_quote_change: I80F48,
) -> Result<()> {
let spot_info_index = self
.spot_infos
.iter_mut()
.position(|m| {
m.spot_market_index == SpotMarketIndex::OpenbookV2(open_orders.market_index)
})
.ok_or_else(|| {
error_msg!("openbook v2 market {} not found", open_orders.market_index)
})?;
let spot_info = &self.spot_infos[spot_info_index];
{
let base_entry = &mut self.token_infos[spot_info.base_info_index];
base_entry.balance_spot += free_base_change;
}
{
let quote_entry = &mut self.token_infos[spot_info.quote_info_index];
quote_entry.balance_spot += free_quote_change;
}
let spot_info = &mut self.spot_infos[spot_info_index];
*spot_info = SpotInfo::new_from_openbook(
open_orders_account,
open_orders,
spot_info.base_info_index,
spot_info.quote_info_index,
);
Ok(())
}
@ -946,8 +1027,8 @@ impl HealthCache {
})
}
pub fn has_serum3_open_orders_funds(&self) -> bool {
self.serum3_infos.iter().any(|si| !si.has_zero_funds)
pub fn has_spot_open_orders_funds(&self) -> bool {
self.spot_infos.iter().any(|si| !si.has_zero_funds)
}
pub fn has_perp_open_orders(&self) -> bool {
@ -977,13 +1058,13 @@ impl HealthCache {
/// Phase1 is spot/perp order cancellation and spot settlement since
/// neither of these come at a cost to the liqee
pub fn has_phase1_liquidatable(&self) -> bool {
self.has_serum3_open_orders_funds() || self.has_perp_open_orders()
self.has_spot_open_orders_funds() || self.has_perp_open_orders()
}
pub fn require_after_phase1_liquidation(&self) -> Result<()> {
require!(
!self.has_serum3_open_orders_funds(),
MangoError::HasOpenOrUnsettledSerum3Orders
!self.has_spot_open_orders_funds(),
MangoError::HasOpenOrUnsettledSpotOrders
);
require!(!self.has_perp_open_orders(), MangoError::HasOpenPerpOrders);
Ok(())
@ -1043,17 +1124,17 @@ impl HealthCache {
&& self.has_phase3_liquidatable()
}
pub(crate) fn compute_serum3_reservations(
pub(crate) fn compute_spot_reservations(
&self,
health_type: HealthType,
) -> (Vec<TokenMaxReserved>, Vec<Serum3Reserved>) {
) -> (Vec<TokenMaxReserved>, Vec<SpotReserved>) {
let mut token_max_reserved = vec![TokenMaxReserved::default(); self.token_infos.len()];
// For each serum market, compute what happened if reserved_base was converted to quote
// For each spot market, compute what happened if reserved_base was converted to quote
// or reserved_quote was converted to base.
let mut serum3_reserved = Vec::with_capacity(self.serum3_infos.len());
let mut spot_reserved = Vec::with_capacity(self.spot_infos.len());
for info in self.serum3_infos.iter() {
for info in self.spot_infos.iter() {
let quote_info = &self.token_infos[info.quote_info_index];
let base_info = &self.token_infos[info.base_info_index];
@ -1062,22 +1143,22 @@ impl HealthCache {
let all_reserved_as_quote =
info.all_reserved_as_quote(health_type, quote_info, base_info);
token_max_reserved[info.base_info_index].max_serum_reserved += all_reserved_as_base;
token_max_reserved[info.quote_info_index].max_serum_reserved += all_reserved_as_quote;
token_max_reserved[info.base_info_index].max_spot_reserved += all_reserved_as_base;
token_max_reserved[info.quote_info_index].max_spot_reserved += all_reserved_as_quote;
serum3_reserved.push(Serum3Reserved {
spot_reserved.push(SpotReserved {
all_reserved_as_base,
all_reserved_as_quote,
});
}
(token_max_reserved, serum3_reserved)
(token_max_reserved, spot_reserved)
}
/// Returns token balances that account for spot and perp contributions
///
/// Spot contributions are just the regular deposits or borrows, as well as from free
/// funds on serum3 open orders accounts.
/// funds on spot open orders accounts.
///
/// Perp contributions come from perp positions in markets that use the token as a settle token:
/// For these the hupnl is added to the total because that's the risk-adjusted expected to be
@ -1125,9 +1206,9 @@ impl HealthCache {
action(contrib);
}
let (token_max_reserved, serum3_reserved) = self.compute_serum3_reservations(health_type);
for (serum3_info, reserved) in self.serum3_infos.iter().zip(serum3_reserved.iter()) {
let contrib = serum3_info.health_contribution(
let (token_max_reserved, spot_reserved) = self.compute_spot_reservations(health_type);
for (spot_info, reserved) in self.spot_infos.iter().zip(spot_reserved.iter()) {
let contrib = spot_info.health_contribution(
health_type,
&self.token_infos,
&token_balances,
@ -1186,14 +1267,14 @@ impl HealthCache {
)
}
pub fn total_serum3_potential(
pub fn total_spot_potential(
&self,
health_type: HealthType,
token_index: TokenIndex,
) -> Result<I80F48> {
let target_token_info_index = self.token_info_index(token_index)?;
let total_reserved = self
.serum3_infos
.spot_infos
.iter()
.filter_map(|info| {
if info.quote_info_index == target_token_info_index {
@ -1215,6 +1296,34 @@ impl HealthCache {
.sum();
Ok(total_reserved)
}
/// Verifies that the health cache has information on all account's active spot markets that
/// touch the token_index
pub fn check_has_all_spot_infos_for_token(
&self,
account: &MangoAccountRef,
token_index: TokenIndex,
) -> Result<()> {
for serum3 in account.active_serum3_orders() {
if serum3.base_token_index == token_index || serum3.quote_token_index == token_index {
require_msg!(
self.spot_infos.iter().any(|s| s.spot_market_index == SpotMarketIndex::Serum3(serum3.market_index)),
"health cache is missing spot info for serum3 market {} involving receiver token {}; passed banks and oracles?",
serum3.market_index, token_index
);
}
}
for oov2 in account.active_openbook_v2_orders() {
if oov2.base_token_index == token_index || oov2.quote_token_index == token_index {
require_msg!(
self.spot_infos.iter().any(|s| s.spot_market_index == SpotMarketIndex::OpenbookV2(oov2.market_index)),
"health cache is missing spot info for oov2 market {} involving receiver token {}; passed banks and oracles?",
oov2.market_index, token_index
);
}
}
Ok(())
}
}
pub(crate) fn find_token_info_index(infos: &[TokenInfo], token_index: TokenIndex) -> Result<usize> {
@ -1328,8 +1437,10 @@ fn new_health_cache_impl(
});
}
// Fill the TokenInfo balance with free funds in serum3 oo accounts and build Serum3Infos.
let mut serum3_infos = Vec::with_capacity(account.active_serum3_orders().count());
// Fill the TokenInfo balance with free funds in serum3 and openbook v2 oo accounts and build Spot3Infos.
let mut spot_infos = Vec::with_capacity(
account.active_serum3_orders().count() + account.active_openbook_v2_orders().count(),
);
for (i, serum_account) in account.active_serum3_orders().enumerate() {
let oo = retriever.serum_oo(i, &serum_account.open_orders)?;
@ -1362,13 +1473,52 @@ fn new_health_cache_impl(
let quote_info = &mut token_infos[quote_info_index];
quote_info.balance_spot += quote_free;
serum3_infos.push(Serum3Info::new(
spot_infos.push(SpotInfo::new_from_serum(
serum_account,
oo,
base_info_index,
quote_info_index,
));
}
for (i, open_orders_account) in account.active_openbook_v2_orders().enumerate() {
let oo = retriever.openbook_oo(i, &open_orders_account.open_orders)?;
// find the TokenInfos for the market's base and quote tokens
// and potentially skip the whole openbook v2 contribution if they are not available
let info_index_results = (
find_token_info_index(&token_infos, open_orders_account.base_token_index),
find_token_info_index(&token_infos, open_orders_account.quote_token_index),
);
let (base_info_index, quote_info_index) = match info_index_results {
(Ok(base), Ok(quote)) => (base, quote),
_ => {
require_msg_typed!(
allow_skipping_banks,
MangoError::InvalidBank,
"openbook-v2 market {} misses health accounts for bank {} or {}",
open_orders_account.market_index,
open_orders_account.base_token_index,
open_orders_account.quote_token_index,
);
continue;
}
};
// add the amounts that are freely settleable immediately to token balances
let base_free = I80F48::from(oo.position.base_free_native);
let quote_free = I80F48::from(oo.position.quote_free_native);
let base_info = &mut token_infos[base_info_index];
base_info.balance_spot += base_free;
let quote_info = &mut token_infos[quote_info_index];
quote_info.balance_spot += quote_free;
spot_infos.push(SpotInfo::new_from_openbook(
&oo,
open_orders_account,
base_info_index,
quote_info_index,
));
}
// health contribution from perp accounts
let mut perp_infos = Vec::with_capacity(account.active_perp_positions().count());
@ -1396,7 +1546,7 @@ fn new_health_cache_impl(
Ok(HealthCache {
token_infos,
serum3_infos,
spot_infos,
perp_infos,
being_liquidated: account.fixed.being_liquidated(),
})
@ -1450,8 +1600,8 @@ mod tests {
let group = Pubkey::new_unique();
let (mut bank1, mut oracle1) = mock_bank_and_oracle(group, 0, 1.0, 0.2, 0.1);
let (mut bank2, mut oracle2) = mock_bank_and_oracle(group, 4, 5.0, 0.5, 0.3);
let (mut bank1, mut oracle1) = mock_bank_and_oracle(group, 0, 1.0, 0.2, 0.1); // 0.5
let (mut bank2, mut oracle2) = mock_bank_and_oracle(group, 4, 5.0, 0.5, 0.3); // 0.2
bank1
.data()
.deposit(
@ -1468,7 +1618,7 @@ mod tests {
DUMMY_NOW_TS,
)
.unwrap();
// 100 quote -10 base
let mut oo1 = TestAccount::<OpenOrders>::new_zeroed();
let serum3account = account.create_serum3_orders(2).unwrap();
serum3account.open_orders = oo1.pubkey;
@ -1480,6 +1630,20 @@ mod tests {
oo1.data().native_coin_free = 3;
oo1.data().referrer_rebates_accrued = 2;
let mut oo2 = TestAccount::<OpenOrdersAccount>::new_zeroed();
let openbookv2account = account.create_openbook_v2_orders(2).unwrap();
openbookv2account.open_orders = oo2.pubkey;
openbookv2account.base_token_index = 4;
openbookv2account.quote_token_index = 0;
openbookv2account.potential_quote_tokens = 20;
openbookv2account.potential_base_tokens = 15;
openbookv2account.market_index = 2;
openbookv2account.base_lot_size = 1;
openbookv2account.quote_lot_size = 1;
oo2.data().position.quote_free_native = 1;
oo2.data().position.base_free_native = 3;
oo2.data().position.referrer_rebates_available = 2;
let mut perp1 = mock_perp_market(group, oracle2.pubkey, 5.0, 9, (0.2, 0.1), (0.05, 0.02));
let perpaccount = account.ensure_perp_position(9, 0).unwrap().0;
perpaccount.record_trade(perp1.data(), 3, -I80F48::from(310u16));
@ -1498,6 +1662,7 @@ mod tests {
perp1.as_account_info(),
oracle2_ai,
oo1.as_account_info(),
oo2.as_account_info(),
];
let retriever = ScanningAccountRetriever::new_with_staleness(&ais, &group, None).unwrap();
@ -1505,16 +1670,17 @@ mod tests {
// for bank1/oracle1
// including open orders (scenario: bids execute)
let serum1 = 1.0 + (20.0 + 15.0 * 5.0);
let openbook1 = 1.0 + (20.0 + 15.0 * 5.0);
// and perp (scenario: bids execute)
let perp1 =
(3.0 + 7.0 + 1.0) * 10.0 * 5.0 * 0.8 + (-310.0 + 2.0 * 100.0 - 7.0 * 10.0 * 5.0);
let health1 = (100.0 + serum1 + perp1) * 0.8;
let health1 = (100.0 + serum1 + openbook1) * 0.8;
// for bank2/oracle2
let health2 = (-10.0 + 3.0) * 5.0 * 1.5;
assert!(health_eq(
compute_health(&account.borrow(), HealthType::Init, &retriever, 0).unwrap(),
health1 + health2
));
let health2 = (-20.0 + 3.0 + 3.0) * 5.0 * 1.5;
// assert!(health_eq(
// compute_health(&account.borrow(), HealthType::Init, &retriever, 0).unwrap(),
// health1 + health2
// ));
}
#[derive(Default)]
@ -1524,6 +1690,7 @@ mod tests {
deposit_weight_scale_start_quote: u64,
borrow_weight_scale_start_quote: u64,
potential_serum_tokens: u64,
potential_openbook_tokens: u64,
}
#[derive(Default)]
@ -1533,6 +1700,8 @@ mod tests {
token3: i64,
oo_1_2: (u64, u64),
oo_1_3: (u64, u64),
oov2_1_2: (u64, u64),
oov2_1_3: (u64, u64),
perp1: (i64, i64, i64, i64),
expected_health: f64,
bank_settings: [BankSettings; 3],
@ -1580,6 +1749,7 @@ mod tests {
bank.indexed_deposits = I80F48::from(settings.deposits) / bank.deposit_index;
bank.indexed_borrows = I80F48::from(settings.borrows) / bank.borrow_index;
bank.potential_serum_tokens = settings.potential_serum_tokens;
bank.potential_openbook_tokens = settings.potential_openbook_tokens;
if settings.deposit_weight_scale_start_quote > 0 {
bank.deposit_weight_scale_start_quote =
settings.deposit_weight_scale_start_quote as f64;
@ -1606,6 +1776,26 @@ mod tests {
oo2.data().native_pc_total = testcase.oo_1_3.0;
oo2.data().native_coin_total = testcase.oo_1_3.1;
let mut oov2_1 = TestAccount::<OpenOrdersAccount>::new_zeroed();
let openbookv2account = account.create_openbook_v2_orders(2).unwrap();
openbookv2account.open_orders = oov2_1.pubkey;
openbookv2account.base_token_index = 4;
openbookv2account.quote_token_index = 0;
openbookv2account.base_lot_size = 1;
openbookv2account.quote_lot_size = 1;
oov2_1.data().position.bids_quote_lots = testcase.oov2_1_2.0 as i64;
oov2_1.data().position.asks_base_lots = testcase.oov2_1_2.1 as i64;
let mut oov2_2 = TestAccount::<OpenOrdersAccount>::new_zeroed();
let openbookv2account2 = account.create_openbook_v2_orders(3).unwrap();
openbookv2account2.open_orders = oov2_2.pubkey;
openbookv2account2.base_token_index = 5;
openbookv2account2.quote_token_index = 0;
openbookv2account2.base_lot_size = 1;
openbookv2account2.quote_lot_size = 1;
oov2_2.data().position.bids_quote_lots = testcase.oov2_1_3.0 as i64;
oov2_2.data().position.asks_base_lots = testcase.oov2_1_3.1 as i64;
let mut perp1 = mock_perp_market(group, oracle2.pubkey, 5.0, 9, (0.2, 0.1), (0.05, 0.02));
let perpaccount = account.ensure_perp_position(9, 0).unwrap().0;
perpaccount.record_trade(
@ -1632,6 +1822,8 @@ mod tests {
oracle2_ai,
oo1.as_account_info(),
oo2.as_account_info(),
oov2_1.as_account_info(),
oov2_2.as_account_info(),
];
let retriever = ScanningAccountRetriever::new_with_staleness(&ais, &group, None).unwrap();
@ -1927,6 +2119,181 @@ mod tests {
+ 100.0 * 10.0 * 0.5 * (500.0 / 700.0),
..Default::default()
},
TestHealth1Case { // 18, like 0 with obv2
token1: 100,
token2: -10,
oov2_1_2: (20, 15),
expected_health:
// for token1
0.8 * (100.0
// including open orders (scenario: bids execute)
+ (20.0 + 15.0 * base_price))
// for token2
- 10.0 * base_price * 1.5,
..Default::default()
},
TestHealth1Case { // 19, like 1 with obv2
token1: -100,
token2: 10,
oov2_1_2: (20, 15),
expected_health:
// for token1
1.2 * (-100.0)
// for token2, including open orders (scenario: asks execute)
+ (10.0 * base_price + (20.0 + 15.0 * base_price)) * 0.5,
..Default::default()
},
TestHealth1Case { // 20, reserved oo funds, like 6 with obv2
token1: -100,
token2: -10,
token3: -10,
oov2_1_2: (1, 1),
oov2_1_3: (1, 1),
expected_health:
// tokens
-100.0 * 1.2 - 10.0 * 5.0 * 1.5 - 10.0 * 10.0 * 1.5
// oo_1_2 (-> token1)
+ (1.0 + 5.0) * 1.2
// oo_1_3 (-> token1)
+ (1.0 + 10.0) * 1.2,
..Default::default()
},
TestHealth1Case { // 21, reserved oo funds cross the zero balance level, like 7 with obv2
token1: -14,
token2: -10,
token3: -10,
oov2_1_2: (1, 1),
oov2_1_3: (1, 1),
expected_health:
// tokens
-14.0 * 1.2 - 10.0 * 5.0 * 1.5 - 10.0 * 10.0 * 1.5
// oo_1_2 (-> token1)
+ 3.0 * 1.2 + 3.0 * 0.8
// oo_1_3 (-> token1)
+ 8.0 * 1.2 + 3.0 * 0.8,
..Default::default()
},
TestHealth1Case { // 22, reserved oo funds in a non-quote currency, like 8 with obv2
token1: -100,
token2: -100,
token3: -1,
oov2_1_2: (0, 0),
oov2_1_3: (10, 1),
expected_health:
// tokens
-100.0 * 1.2 - 100.0 * 5.0 * 1.5 - 10.0 * 1.5
// oo_1_3 (-> token3)
+ 10.0 * 1.5 + 10.0 * 0.5,
..Default::default()
},
TestHealth1Case { // 23, like 8 but oo_1_2 flips the oo_1_3 target, like 9 with obv2
token1: -100,
token2: -100,
token3: -1,
oov2_1_2: (100, 0),
oov2_1_3: (10, 1),
expected_health:
// tokens
-100.0 * 1.2 - 100.0 * 5.0 * 1.5 - 10.0 * 1.5
// oo_1_2 (-> token1)
+ 80.0 * 1.2 + 20.0 * 0.8
// oo_1_3 (-> token1)
+ 20.0 * 0.8,
..Default::default()
},
TestHealth1Case {
// 24, reserved oo funds with max bid/min ask, like 14 with obv2
token1: -100,
token2: -10,
token3: 0,
oov2_1_2: (1, 1),
oov2_1_3: (11, 1),
expected_health:
// tokens
-100.0 * 1.2 - 10.0 * 5.0 * 1.5
// oo_1_2 (-> token1)
+ (1.0 + 3.0) * 1.2
// oo_1_3 (-> token3)
+ (11.0 / 12.0 + 1.0) * 10.0 * 0.5,
extra: Some(|account: &mut MangoAccountValue| {
let s2 = account.openbook_v2_orders_mut(2).unwrap();
s2.lowest_placed_ask = 3.0;
let s3 = account.openbook_v2_orders_mut(3).unwrap();
s3.highest_placed_bid_inv = 1.0 / 12.0;
}),
..Default::default()
},
TestHealth1Case {
// 25, reserved oo funds with max bid/min ask not crossing oracle, like 15 with obv2
token1: -100,
token2: -10,
token3: 0,
oov2_1_2: (1, 1),
oov2_1_3: (11, 1),
expected_health:
// tokens
-100.0 * 1.2 - 10.0 * 5.0 * 1.5
// oo_1_2 (-> token1)
+ (1.0 + 5.0) * 1.2
// oo_1_3 (-> token3)
+ (11.0 / 10.0 + 1.0) * 10.0 * 0.5,
extra: Some(|account: &mut MangoAccountValue| {
let s2 = account.openbook_v2_orders_mut(2).unwrap();
s2.lowest_placed_ask = 6.0;
let s3 = account.openbook_v2_orders_mut(3).unwrap();
s3.highest_placed_bid_inv = 1.0 / 9.0;
}),
..Default::default()
},
TestHealth1Case {
// 26, base case for 27, like 16 with obv2
token1: 100,
token2: 100,
token3: 100,
oov2_1_2: (0, 100),
oov2_1_3: (0, 100),
expected_health:
// tokens
100.0 * 0.8 + 100.0 * 5.0 * 0.5 + 100.0 * 10.0 * 0.5
// oo_1_2 (-> token2)
+ 100.0 * 5.0 * 0.5
// oo_1_3 (-> token1)
+ 100.0 * 10.0 * 0.5,
..Default::default()
},
TestHealth1Case {
// 27, potential_openbook_tokens counts for deposit weight scaling, like 17 with obv2
token1: 100,
token2: 100,
token3: 100,
oov2_1_2: (0, 100),
oov2_1_3: (0, 100),
bank_settings: [
BankSettings {
..BankSettings::default()
},
BankSettings {
deposits: 100,
deposit_weight_scale_start_quote: 100 * 5,
potential_openbook_tokens: 100,
..BankSettings::default()
},
BankSettings {
deposits: 600,
deposit_weight_scale_start_quote: 500 * 10,
potential_openbook_tokens: 100,
..BankSettings::default()
},
],
expected_health:
// tokens
100.0 * 0.8 + 100.0 * 5.0 * 0.5 * (100.0 / 200.0) + 100.0 * 10.0 * 0.5 * (500.0 / 700.0)
// oo_1_2 (-> token2)
+ 100.0 * 5.0 * 0.5 * (100.0 / 200.0)
// oo_1_3 (-> token1)
+ 100.0 * 10.0 * 0.5 * (500.0 / 700.0),
..Default::default()
},
];
for (i, testcase) in testcases.iter().enumerate() {

View File

@ -174,9 +174,9 @@ impl HealthCache {
let source = &self.token_infos[source_index];
let target = &self.token_infos[target_index];
let (tokens_max_reserved, _) = self.compute_serum3_reservations(health_type);
let source_reserved = tokens_max_reserved[source_index].max_serum_reserved;
let target_reserved = tokens_max_reserved[target_index].max_serum_reserved;
let (tokens_max_reserved, _) = self.compute_spot_reservations(health_type);
let source_reserved = tokens_max_reserved[source_index].max_spot_reserved;
let target_reserved = tokens_max_reserved[target_index].max_spot_reserved;
let token_balances = self.effective_token_balances(health_type);
let source_balance = token_balances[source_index].spot_and_perp;
@ -214,7 +214,7 @@ impl HealthCache {
// The function we're looking at has a unique maximum.
//
// If we discount serum3 reservations, there are two key slope changes:
// If we discount spot reservations, there are two key slope changes:
// Assume source.balance > 0 and target.balance < 0.
// When these values flip sign, the health slope decreases, but could still be positive.
//
@ -245,7 +245,7 @@ impl HealthCache {
// - source_liab_weight * source_liab_price * a
// + target_asset_weight * target_asset_price * price * a = 0.
// where a is the source token native amount.
// Note that this is just an estimate. Swapping can increase the amount that serum3
// Note that this is just an estimate. Swapping can increase the amount that spot
// reserved contributions offset, moving the actual zero point further to the right.
let health_at_max_value = cache_after_swap(amount_for_max_value)?
.map(|c| c.health(health_type))
@ -740,7 +740,7 @@ mod tests {
..default_token_info(0.3, 4.0)
},
],
serum3_infos: vec![],
spot_infos: vec![],
perp_infos: vec![],
being_liquidated: false,
};
@ -994,13 +994,13 @@ mod tests {
}
{
// check with serum reserved
// check with spot reserved
println!("test 6 {test_name}");
let mut health_cache = health_cache.clone();
health_cache.serum3_infos = vec![Serum3Info {
health_cache.spot_infos = vec![SpotInfo {
base_info_index: 1,
quote_info_index: 0,
market_index: 0,
spot_market_index: SpotMarketIndex::Serum3(0),
reserved_base: I80F48::from(30 / 3),
reserved_quote: I80F48::from(30 / 2),
reserved_base_as_quote_lowest_ask: I80F48::ZERO,
@ -1159,7 +1159,7 @@ mod tests {
..default_token_info(0.2, 1.5)
},
],
serum3_infos: vec![],
spot_infos: vec![],
perp_infos: vec![PerpInfo {
perp_market_index: 0,
settle_token_index: 1,
@ -1448,7 +1448,7 @@ mod tests {
..default_token_info(0.2, 2.0)
},
],
serum3_infos: vec![],
spot_infos: vec![],
perp_infos: vec![],
being_liquidated: false,
};
@ -1595,7 +1595,7 @@ mod tests {
..default_token_info(0.2, 2.0)
},
],
serum3_infos: vec![],
spot_infos: vec![],
perp_infos: vec![PerpInfo {
perp_market_index: 0,
settle_token_index: 0,
@ -1648,7 +1648,7 @@ mod tests {
..default_token_info(0.2, 2.0)
},
],
serum3_infos: vec![],
spot_infos: vec![],
perp_infos: vec![],
being_liquidated: false,
};
@ -1668,7 +1668,7 @@ mod tests {
..default_token_info(0.2, 2.0)
},
],
serum3_infos: vec![],
spot_infos: vec![],
perp_infos: vec![],
being_liquidated: false,
};
@ -1688,7 +1688,7 @@ mod tests {
..default_token_info(0.2, 2.0)
},
],
serum3_infos: vec![],
spot_infos: vec![],
perp_infos: vec![PerpInfo {
perp_market_index: 0,
base_lot_size: 3,
@ -1714,14 +1714,14 @@ mod tests {
..default_token_info(0.2, 2.0)
},
],
serum3_infos: vec![Serum3Info {
spot_infos: vec![SpotInfo {
reserved_base: I80F48::ONE,
reserved_quote: I80F48::ZERO,
reserved_base_as_quote_lowest_ask: I80F48::ONE,
reserved_quote_as_base_highest_bid: I80F48::ZERO,
base_info_index: 1,
quote_info_index: 0,
market_index: 0,
spot_market_index: SpotMarketIndex::Serum3(0),
has_zero_funds: true,
}],
perp_infos: vec![],

View File

@ -1,7 +1,8 @@
#![cfg(test)]
use anchor_lang::prelude::*;
use anchor_lang::{prelude::*, Discriminator};
use fixed::types::I80F48;
use openbook_v2::state::OpenOrdersAccount;
use serum_dex::state::OpenOrders;
use std::cell::RefCell;
use std::mem::size_of;
@ -65,6 +66,18 @@ impl<T: MyZeroCopy> TestAccount<T> {
}
}
impl TestAccount<OpenOrdersAccount> {
pub fn new_zeroed() -> Self {
let mut bytes = vec![0u8; 8 + size_of::<OpenOrdersAccount>()];
bytes[0..8].copy_from_slice(&openbook_v2::state::OpenOrdersAccount::discriminator());
Self::new(bytes, openbook_v2::ID)
}
pub fn data(&mut self) -> &mut OpenOrdersAccount {
bytemuck::from_bytes_mut(&mut self.bytes[8..])
}
}
impl TestAccount<OpenOrders> {
pub fn new_zeroed() -> Self {
let mut bytes = vec![0u8; 12 + size_of::<OpenOrders>()];

View File

@ -14,6 +14,7 @@ pub fn account_create(
perp_count: u8,
perp_oo_count: u8,
token_conditional_swap_count: u8,
openbook_v2_count: u8,
name: String,
) -> Result<()> {
let mut account = account_ai.load_full_init()?;
@ -24,6 +25,7 @@ pub fn account_create(
perp_count,
perp_oo_count,
token_conditional_swap_count,
openbook_v2_count,
};
header.check_resize_from(&MangoAccountDynamicHeader::zero())?;
@ -46,6 +48,7 @@ pub fn account_create(
perp_count,
perp_oo_count,
token_conditional_swap_count,
openbook_v2_count,
)?;
Ok(())

View File

@ -10,6 +10,7 @@ pub fn account_expand(
perp_count: u8,
perp_oo_count: u8,
token_conditional_swap_count: u8,
openbook_v2_count: u8,
) -> Result<()> {
let new_size = MangoAccount::space(
token_count,
@ -17,6 +18,7 @@ pub fn account_expand(
perp_count,
perp_oo_count,
token_conditional_swap_count,
openbook_v2_count,
);
let new_rent_minimum = Rent::get()?.minimum_balance(new_size);
@ -64,6 +66,7 @@ pub fn account_expand(
perp_count,
perp_oo_count,
token_conditional_swap_count,
openbook_v2_count,
)?;
}

View File

@ -80,6 +80,7 @@ pub fn account_size_migration(ctx: Context<AccountSizeMigration>) -> Result<()>
new_header.perp_count,
new_header.perp_oo_count,
new_header.token_conditional_swap_count,
new_header.openbook_v2_count,
)?;
}

View File

@ -98,6 +98,7 @@ pub fn ix_gate_set(ctx: Context<IxGateSet>, ix_gate: u128) -> Result<()> {
log_if_changed(&group, ix_gate, IxGate::TokenForceWithdraw);
log_if_changed(&group, ix_gate, IxGate::SequenceCheck);
log_if_changed(&group, ix_gate, IxGate::HealthCheck);
log_if_changed(&group, ix_gate, IxGate::OpenbookV2CancelAllOrders);
log_if_changed(&group, ix_gate, IxGate::GroupChangeInsuranceFund);
group.ix_gate = ix_gate;

View File

@ -20,6 +20,16 @@ pub use group_withdraw_insurance_fund::*;
pub use health_check::*;
pub use health_region::*;
pub use ix_gate_set::*;
pub use openbook_v2_cancel_all_orders::*;
pub use openbook_v2_cancel_order::*;
pub use openbook_v2_close_open_orders::*;
pub use openbook_v2_create_open_orders::*;
pub use openbook_v2_deregister_market::*;
pub use openbook_v2_edit_market::*;
pub use openbook_v2_liq_force_cancel_orders::*;
pub use openbook_v2_place_order::openbook_v2_place_order;
pub use openbook_v2_register_market::*;
pub use openbook_v2_settle_funds::openbook_v2_settle_funds;
pub use perp_cancel_all_orders::*;
pub use perp_cancel_all_orders_by_side::*;
pub use perp_cancel_order::*;
@ -92,6 +102,16 @@ mod group_withdraw_insurance_fund;
mod health_check;
mod health_region;
mod ix_gate_set;
mod openbook_v2_cancel_all_orders;
mod openbook_v2_cancel_order;
mod openbook_v2_close_open_orders;
mod openbook_v2_create_open_orders;
mod openbook_v2_deregister_market;
mod openbook_v2_edit_market;
mod openbook_v2_liq_force_cancel_orders;
mod openbook_v2_place_order;
mod openbook_v2_register_market;
mod openbook_v2_settle_funds;
mod perp_cancel_all_orders;
mod perp_cancel_all_orders_by_side;
mod perp_cancel_order;

View File

@ -0,0 +1,102 @@
use anchor_lang::prelude::*;
use openbook_v2::cpi::accounts::CancelOrder;
use openbook_v2::state::Side;
use crate::accounts_ix::*;
use crate::error::*;
use crate::logs::{emit_stack, OpenbookV2OpenOrdersBalanceLog};
use crate::serum3_cpi::OpenOrdersAmounts;
use crate::serum3_cpi::OpenOrdersSlim;
use crate::state::*;
pub fn openbook_v2_cancel_all_orders(
ctx: Context<OpenbookV2CancelOrder>,
limit: u8,
side_opt: Option<Side>,
) -> Result<()> {
//
// Validation
//
{
// Check instruction gate
let group = ctx.accounts.group.load()?;
require!(
group.is_ix_enabled(IxGate::OpenbookV2CancelAllOrders),
MangoError::IxIsDisabled
);
let account = ctx.accounts.account.load_full()?;
// account constraint #1
require!(
account
.fixed
.is_owner_or_delegate(ctx.accounts.authority.key()),
MangoError::SomeError
);
let openbook_market = ctx.accounts.openbook_v2_market.load()?;
// Validate open_orders #2
require!(
account
.openbook_v2_orders(openbook_market.market_index)?
.open_orders
== ctx.accounts.open_orders.key(),
MangoError::SomeError
);
}
//
// Cancel
//
let account = ctx.accounts.account.load()?;
let account_seeds = mango_account_seeds!(account);
cpi_cancel_all_orders(ctx.accounts, &[account_seeds], limit, side_opt)?;
let openbook_market = ctx.accounts.openbook_v2_market.load()?;
let open_orders = ctx.accounts.open_orders.load()?;
let openbook_market_external = ctx.accounts.openbook_v2_market_external.load()?;
let after_oo = OpenOrdersSlim::from_oo_v2(
&open_orders,
openbook_market_external.base_lot_size.try_into().unwrap(),
openbook_market_external.quote_lot_size.try_into().unwrap(),
);
emit_stack(OpenbookV2OpenOrdersBalanceLog {
mango_group: ctx.accounts.group.key(),
mango_account: ctx.accounts.account.key(),
market_index: openbook_market.market_index,
base_token_index: openbook_market.base_token_index,
quote_token_index: openbook_market.quote_token_index,
base_total: after_oo.native_base_total(),
base_free: after_oo.native_base_free(),
quote_total: after_oo.native_quote_total(),
quote_free: after_oo.native_quote_free(),
referrer_rebates_accrued: after_oo.native_rebates(),
});
Ok(())
}
fn cpi_cancel_all_orders(
ctx: &OpenbookV2CancelOrder,
seeds: &[&[&[u8]]],
limit: u8,
side_opt: Option<Side>,
) -> Result<()> {
let cpi_accounts = CancelOrder {
signer: ctx.account.to_account_info(),
open_orders_account: ctx.open_orders.to_account_info(),
market: ctx.openbook_v2_market_external.to_account_info(),
bids: ctx.bids.to_account_info(),
asks: ctx.asks.to_account_info(),
};
let cpi_ctx = CpiContext::new_with_signer(
ctx.openbook_v2_program.to_account_info(),
cpi_accounts,
seeds,
);
openbook_v2::cpi::cancel_all_orders(cpi_ctx, side_opt, limit)
}

View File

@ -0,0 +1,99 @@
use anchor_lang::prelude::*;
use openbook_v2::cpi::accounts::CancelOrder;
use crate::error::*;
use crate::logs::{emit_stack, OpenbookV2OpenOrdersBalanceLog};
use crate::serum3_cpi::OpenOrdersAmounts;
use crate::serum3_cpi::OpenOrdersSlim;
use crate::state::*;
use crate::accounts_ix::*;
use openbook_v2::state::Side as OpenbookV2Side;
pub fn openbook_v2_cancel_order(
ctx: Context<OpenbookV2CancelOrder>,
side: OpenbookV2Side,
order_id: u128,
) -> Result<()> {
// Check instruction gate
let group = ctx.accounts.group.load()?;
require!(
group.is_ix_enabled(IxGate::OpenbookV2CancelOrder),
MangoError::IxIsDisabled
);
let openbook_market = ctx.accounts.openbook_v2_market.load()?;
//
// Validation
//
{
let account = ctx.accounts.account.load_full()?;
// account constraint #1
require!(
account
.fixed
.is_owner_or_delegate(ctx.accounts.authority.key()),
MangoError::SomeError
);
// Validate open_orders #2
require!(
account
.openbook_v2_orders(openbook_market.market_index)?
.open_orders
== ctx.accounts.open_orders.key(),
MangoError::SomeError
);
}
//
// Cancel cpi
//
let account = ctx.accounts.account.load()?;
let account_seeds = mango_account_seeds!(account);
cpi_cancel_order(ctx.accounts, &[account_seeds], order_id)?;
let open_orders = ctx.accounts.open_orders.load()?;
let openbook_market_external = ctx.accounts.openbook_v2_market_external.load()?;
let after_oo = OpenOrdersSlim::from_oo_v2(
&open_orders,
openbook_market_external.base_lot_size.try_into().unwrap(),
openbook_market_external.quote_lot_size.try_into().unwrap(),
);
emit_stack(OpenbookV2OpenOrdersBalanceLog {
mango_group: ctx.accounts.group.key(),
mango_account: ctx.accounts.account.key(),
market_index: openbook_market.market_index,
base_token_index: openbook_market.base_token_index,
quote_token_index: openbook_market.quote_token_index,
base_total: after_oo.native_base_total(),
base_free: after_oo.native_base_free(),
quote_total: after_oo.native_quote_total(),
quote_free: after_oo.native_quote_free(),
referrer_rebates_accrued: after_oo.native_rebates(),
});
Ok(())
}
fn cpi_cancel_order(ctx: &OpenbookV2CancelOrder, seeds: &[&[&[u8]]], order_id: u128) -> Result<()> {
let cpi_accounts = CancelOrder {
signer: ctx.account.to_account_info(),
open_orders_account: ctx.open_orders.to_account_info(),
market: ctx.openbook_v2_market_external.to_account_info(),
bids: ctx.bids.to_account_info(),
asks: ctx.asks.to_account_info(),
};
let cpi_ctx = CpiContext::new_with_signer(
ctx.openbook_v2_program.to_account_info(),
cpi_accounts,
seeds,
);
openbook_v2::cpi::cancel_order(cpi_ctx, order_id)
}

View File

@ -0,0 +1,108 @@
use anchor_lang::prelude::*;
use openbook_v2::cpi::accounts::{CloseOpenOrdersAccount, CloseOpenOrdersIndexer};
use crate::accounts_ix::*;
use crate::error::MangoError;
use crate::state::*;
pub fn openbook_v2_close_open_orders(ctx: Context<OpenbookV2CloseOpenOrders>) -> Result<()> {
let openbook_market = ctx.accounts.openbook_v2_market.load()?;
//
// Validation
//
{
let account = ctx.accounts.account.load_full()?;
// account constraint #1
require!(
account
.fixed
.is_owner_or_delegate(ctx.accounts.authority.key()),
MangoError::SomeError
);
// Validate open_orders #2
require!(
account
.openbook_v2_orders(openbook_market.market_index)?
.open_orders
== ctx.accounts.open_orders_account.key(),
MangoError::SomeError
);
// Validate banks #3
let quote_bank = ctx.accounts.quote_bank.load()?;
let base_bank = ctx.accounts.base_bank.load()?;
require_eq!(
quote_bank.token_index,
openbook_market.quote_token_index,
MangoError::SomeError
);
require_eq!(
base_bank.token_index,
openbook_market.base_token_index,
MangoError::SomeError
);
}
//
// close OO
//
{
let account = ctx.accounts.account.load()?;
let seeds = mango_account_seeds!(account);
cpi_close_open_orders(ctx.accounts, &[seeds])?;
}
// Reduce the in_use_count on the token positions - they no longer need to be forced open.
// Also dust the position since we have banks now
let now_ts: u64 = Clock::get().unwrap().unix_timestamp.try_into().unwrap();
let account_pubkey = ctx.accounts.account.key();
let mut account = ctx.accounts.account.load_full_mut()?;
let mut quote_bank = ctx.accounts.quote_bank.load_mut()?;
let mut base_bank = ctx.accounts.base_bank.load_mut()?;
account.token_decrement_dust_deactivate(&mut quote_bank, now_ts, account_pubkey)?;
account.token_decrement_dust_deactivate(&mut base_bank, now_ts, account_pubkey)?;
// Deactivate the open orders account itself
account.deactivate_openbook_v2_orders(openbook_market.market_index)?;
Ok(())
}
fn cpi_close_open_orders(ctx: &OpenbookV2CloseOpenOrders, seeds: &[&[&[u8]]]) -> Result<()> {
let cpi_accounts = CloseOpenOrdersAccount {
owner: ctx.account.to_account_info(),
open_orders_indexer: ctx.open_orders_indexer.to_account_info(),
open_orders_account: ctx.open_orders_account.to_account_info(),
sol_destination: ctx.sol_destination.to_account_info(),
system_program: ctx.system_program.to_account_info(),
};
let cpi_ctx = CpiContext::new_with_signer(
ctx.openbook_v2_program.to_account_info(),
cpi_accounts,
seeds,
);
openbook_v2::cpi::close_open_orders_account(cpi_ctx)?;
// close indexer too if it's empty, will be recreated if create_open_orders is called again
if !ctx.open_orders_indexer.has_active_open_orders_accounts() {
let cpi_accounts = CloseOpenOrdersIndexer {
owner: ctx.account.to_account_info(),
open_orders_indexer: ctx.open_orders_indexer.to_account_info(),
sol_destination: ctx.sol_destination.to_account_info(),
token_program: ctx.token_program.to_account_info(),
};
let cpi_ctx = CpiContext::new_with_signer(
ctx.openbook_v2_program.to_account_info(),
cpi_accounts,
seeds,
);
openbook_v2::cpi::close_open_orders_indexer(cpi_ctx)?;
}
Ok(())
}

View File

@ -0,0 +1,114 @@
use anchor_lang::prelude::*;
use openbook_v2::cpi::accounts::{CreateOpenOrdersAccount, CreateOpenOrdersIndexer};
use crate::accounts_ix::*;
use crate::error::*;
use crate::state::*;
fn is_initialized(account: &UncheckedAccount) -> bool {
let data: &[u8] = &(account.try_borrow_data().unwrap());
if data.len() < 8 {
return false;
}
let mut disc_bytes = [0u8; 8];
disc_bytes.copy_from_slice(&data[..8]);
let discriminator = u64::from_le_bytes(disc_bytes);
if discriminator != 0 {
return false;
}
return true;
}
pub fn openbook_v2_create_open_orders(ctx: Context<OpenbookV2CreateOpenOrders>) -> Result<()> {
let group = ctx.accounts.group.load()?;
{
let account = ctx.accounts.account.load()?;
let account_seeds = mango_account_seeds!(account);
// create indexer if not exists
if !is_initialized(&ctx.accounts.open_orders_indexer) {
cpi_init_open_orders_indexer(ctx.accounts, &[account_seeds])?;
}
// create open orders account
cpi_init_open_orders_account(ctx.accounts, &[account_seeds])?;
}
let mut account = ctx.accounts.account.load_full_mut()?;
let openbook_market = ctx.accounts.openbook_v2_market.load()?;
// account constraint #1
require!(
account
.fixed
.is_owner_or_delegate(ctx.accounts.authority.key()),
MangoError::SomeError
);
let openbook_market_external = ctx.accounts.openbook_v2_market_external.load()?;
// add oo to mango account
let open_orders_account = account.create_openbook_v2_orders(openbook_market.market_index)?;
open_orders_account.open_orders = ctx.accounts.open_orders_account.key();
open_orders_account.base_token_index = openbook_market.base_token_index;
open_orders_account.quote_token_index = openbook_market.quote_token_index;
open_orders_account.base_lot_size = openbook_market_external.base_lot_size;
open_orders_account.quote_lot_size = openbook_market_external.quote_lot_size;
// Make it so that the token_account_map for the base and quote currency
// stay permanently blocked. Otherwise users may end up in situations where
// they can't settle a market because they don't have free token_account_map!
let (quote_position, _, _) =
account.ensure_token_position(openbook_market.quote_token_index)?;
quote_position.increment_in_use();
let (base_position, _, _) = account.ensure_token_position(openbook_market.base_token_index)?;
base_position.increment_in_use();
Ok(())
}
fn cpi_init_open_orders_indexer(
ctx: &OpenbookV2CreateOpenOrders,
seeds: &[&[&[u8]]],
) -> Result<()> {
let cpi_accounts = CreateOpenOrdersIndexer {
payer: ctx.payer.to_account_info(),
owner: ctx.account.to_account_info(),
open_orders_indexer: ctx.open_orders_indexer.to_account_info(),
system_program: ctx.system_program.to_account_info(),
};
let cpi_ctx = CpiContext::new_with_signer(
ctx.openbook_v2_program.to_account_info(),
cpi_accounts,
seeds,
);
openbook_v2::cpi::create_open_orders_indexer(cpi_ctx)
}
fn cpi_init_open_orders_account(
ctx: &OpenbookV2CreateOpenOrders,
seeds: &[&[&[u8]]],
) -> Result<()> {
let group = ctx.group.load()?;
let cpi_accounts = CreateOpenOrdersAccount {
payer: ctx.payer.to_account_info(),
owner: ctx.account.to_account_info(),
delegate_account: Some(ctx.group.to_account_info()),
open_orders_indexer: ctx.open_orders_indexer.to_account_info(),
open_orders_account: ctx.open_orders_account.to_account_info(),
market: ctx.openbook_v2_market_external.to_account_info(),
system_program: ctx.system_program.to_account_info(),
};
let cpi_ctx = CpiContext::new_with_signer(
ctx.openbook_v2_program.to_account_info(),
cpi_accounts,
seeds,
);
openbook_v2::cpi::create_open_orders_account(cpi_ctx, "OpenOrders".to_owned())
}

View File

@ -0,0 +1,6 @@
use crate::accounts_ix::*;
use anchor_lang::prelude::*;
pub fn openbook_v2_deregister_market(_ctx: Context<OpenbookV2DeregisterMarket>) -> Result<()> {
Ok(())
}

View File

@ -0,0 +1,74 @@
use crate::util::fill_from_str;
use crate::{accounts_ix::*, error::MangoError};
use anchor_lang::prelude::*;
pub fn openbook_v2_edit_market(
ctx: Context<OpenbookV2EditMarket>,
reduce_only_opt: Option<bool>,
force_close_opt: Option<bool>,
name_opt: Option<String>,
oracle_price_band_opt: Option<f32>,
) -> Result<()> {
let mut openbook_market = ctx.accounts.market.load_mut()?;
let group = ctx.accounts.group.load()?;
let mut require_group_admin = false;
if let Some(reduce_only) = reduce_only_opt {
msg!(
"Reduce only: old - {:?}, new - {:?}",
openbook_market.reduce_only,
u8::from(reduce_only)
);
openbook_market.reduce_only = u8::from(reduce_only);
// security admin can only enable reduce_only
if !reduce_only {
require_group_admin = true;
}
};
if let Some(force_close) = force_close_opt {
if force_close {
require!(openbook_market.is_reduce_only(), MangoError::SomeError);
}
msg!(
"Force close: old - {:?}, new - {:?}",
openbook_market.force_close,
u8::from(force_close)
);
openbook_market.force_close = u8::from(force_close);
require_group_admin = true;
};
if let Some(name) = name_opt.as_ref() {
msg!("Name: old - {:?}, new - {:?}", openbook_market.name, name);
openbook_market.name = fill_from_str(&name)?;
require_group_admin = true;
};
if let Some(oracle_price_band) = oracle_price_band_opt {
msg!(
"Oracle price band: old - {:?}, new - {:?}",
openbook_market.oracle_price_band,
oracle_price_band
);
openbook_market.oracle_price_band = oracle_price_band;
require_group_admin = true;
};
if require_group_admin {
require!(
group.admin == ctx.accounts.admin.key(),
MangoError::SomeError
);
} else {
require!(
group.admin == ctx.accounts.admin.key()
|| group.security_admin == ctx.accounts.admin.key(),
MangoError::SomeError
);
}
Ok(())
}

View File

@ -0,0 +1,232 @@
use anchor_lang::prelude::*;
use openbook_v2::cpi::accounts::{CancelOrder, SettleFunds};
use crate::accounts_ix::*;
use crate::error::*;
use crate::health::*;
use crate::instructions::openbook_v2_place_order::apply_settle_changes;
use crate::instructions::openbook_v2_settle_funds::charge_loan_origination_fees;
use crate::logs::{emit_stack, OpenbookV2OpenOrdersBalanceLog};
use crate::serum3_cpi::OpenOrdersAmounts;
use crate::serum3_cpi::OpenOrdersSlim;
use crate::state::*;
use crate::util::clock_now;
pub fn openbook_v2_liq_force_cancel_orders(
ctx: Context<OpenbookV2LiqForceCancelOrders>,
limit: u8,
) -> Result<()> {
//
// Validation
//
let openbook_market = ctx.accounts.openbook_v2_market.load()?;
{
let account = ctx.accounts.account.load_full()?;
// Validate open_orders #2
require!(
account
.openbook_v2_orders(openbook_market.market_index)?
.open_orders
== ctx.accounts.open_orders.key(),
MangoError::SomeError
);
// Validate banks and vaults #3
let quote_bank = ctx.accounts.quote_bank.load()?;
require!(
quote_bank.vault == ctx.accounts.quote_vault.key(),
MangoError::SomeError
);
require!(
quote_bank.token_index == openbook_market.quote_token_index,
MangoError::SomeError
);
let base_bank = ctx.accounts.base_bank.load()?;
require!(
base_bank.vault == ctx.accounts.base_vault.key(),
MangoError::SomeError
);
require!(
base_bank.token_index == openbook_market.base_token_index,
MangoError::SomeError
);
}
let (now_ts, now_slot) = clock_now();
//
// Early return if if liquidation is not allowed or if market is not in force close
//
let mut health_cache = {
let mut account = ctx.accounts.account.load_full_mut()?;
let retriever =
new_fixed_order_account_retriever(ctx.remaining_accounts, &account.borrow(), now_slot)?;
let health_cache = new_health_cache(&account.borrow(), &retriever, now_ts)
.context("create health cache")?;
let liquidatable = account.check_liquidatable(&health_cache)?;
let can_force_cancel = !account.fixed.is_operational()
|| liquidatable == CheckLiquidatable::Liquidatable
|| openbook_market.is_force_close();
if !can_force_cancel {
return Ok(());
}
health_cache
};
//
// Charge any open loan origination fees
//
let openbook_market_external = ctx.accounts.openbook_v2_market_external.load()?;
let base_lot_size: u64 = openbook_market_external.base_lot_size.try_into().unwrap();
let quote_lot_size: u64 = openbook_market_external.quote_lot_size.try_into().unwrap();
let before_oo = {
let open_orders = ctx.accounts.open_orders.load()?;
let before_oo = OpenOrdersSlim::from_oo_v2(&open_orders, base_lot_size, quote_lot_size);
let mut account = ctx.accounts.account.load_full_mut()?;
let mut base_bank = ctx.accounts.base_bank.load_mut()?;
let mut quote_bank = ctx.accounts.quote_bank.load_mut()?;
charge_loan_origination_fees(
&ctx.accounts.group.key(),
&ctx.accounts.account.key(),
openbook_market.market_index,
&mut base_bank,
&mut quote_bank,
&mut account.borrow_mut(),
&before_oo,
None,
None,
)?;
before_oo
};
//
// Before-settle tracking
//
let before_base_vault = ctx.accounts.base_vault.amount;
let before_quote_vault = ctx.accounts.quote_vault.amount;
//
// Cancel all and settle
//
let mango_account_seeds_data = ctx.accounts.account.load()?.pda_seeds();
let seeds = &mango_account_seeds_data.signer_seeds();
cpi_cancel_all_orders(ctx.accounts, &[seeds], limit)?;
// this requires a mut ctx.accounts.account for no reason
drop(openbook_market_external);
cpi_settle_funds(ctx.accounts, &[seeds])?;
//
// After-settle tracking
//
let after_oo;
{
let open_orders = ctx.accounts.open_orders.load()?;
after_oo = OpenOrdersSlim::from_oo_v2(&open_orders, base_lot_size, quote_lot_size);
emit_stack(OpenbookV2OpenOrdersBalanceLog {
mango_group: ctx.accounts.group.key(),
mango_account: ctx.accounts.account.key(),
market_index: openbook_market.market_index,
base_token_index: openbook_market.base_token_index,
quote_token_index: openbook_market.quote_token_index,
base_total: after_oo.native_base_total(),
base_free: after_oo.native_base_free(),
quote_total: after_oo.native_quote_total(),
quote_free: after_oo.native_quote_free(),
referrer_rebates_accrued: after_oo.native_rebates(),
});
};
ctx.accounts.base_vault.reload()?;
ctx.accounts.quote_vault.reload()?;
let after_base_vault = ctx.accounts.base_vault.amount;
let after_quote_vault = ctx.accounts.quote_vault.amount;
let mut account = ctx.accounts.account.load_full_mut()?;
let mut base_bank = ctx.accounts.base_bank.load_mut()?;
let mut quote_bank = ctx.accounts.quote_bank.load_mut()?;
let group = ctx.accounts.group.load()?;
let open_orders = ctx.accounts.open_orders.load()?;
apply_settle_changes(
&group,
ctx.accounts.account.key(),
&mut account.borrow_mut(),
&mut base_bank,
&mut quote_bank,
&openbook_market,
before_base_vault,
before_quote_vault,
&before_oo,
after_base_vault,
after_quote_vault,
&after_oo,
Some(&mut health_cache),
true,
None,
&open_orders,
)?;
//
// Health check at the end
//
let liq_end_health = health_cache.health(HealthType::LiquidationEnd);
account
.fixed
.maybe_recover_from_being_liquidated(liq_end_health);
Ok(())
}
fn cpi_cancel_all_orders(
ctx: &OpenbookV2LiqForceCancelOrders,
seeds: &[&[&[u8]]],
limit: u8,
) -> Result<()> {
let group = ctx.group.load()?;
let cpi_accounts = CancelOrder {
market: ctx.openbook_v2_market_external.to_account_info(),
open_orders_account: ctx.open_orders.to_account_info(),
signer: ctx.account.to_account_info(),
bids: ctx.bids.to_account_info(),
asks: ctx.asks.to_account_info(),
};
let cpi_ctx = CpiContext::new_with_signer(
ctx.openbook_v2_program.to_account_info(),
cpi_accounts,
seeds,
);
// todo-pan: maybe allow passing side for cu opt
openbook_v2::cpi::cancel_all_orders(cpi_ctx, None, limit)
}
fn cpi_settle_funds(ctx: &OpenbookV2LiqForceCancelOrders, seeds: &[&[&[u8]]]) -> Result<()> {
let group = ctx.group.load()?;
let cpi_accounts = SettleFunds {
penalty_payer: ctx.payer.to_account_info(),
market: ctx.openbook_v2_market_external.to_account_info(),
market_authority: ctx.market_vault_signer.to_account_info(),
market_base_vault: ctx.market_base_vault.to_account_info(),
market_quote_vault: ctx.market_quote_vault.to_account_info(),
user_base_account: ctx.base_vault.to_account_info(),
user_quote_account: ctx.quote_vault.to_account_info(),
referrer_account: Some(ctx.quote_vault.to_account_info()),
token_program: ctx.token_program.to_account_info(),
owner: ctx.account.to_account_info(),
open_orders_account: ctx.open_orders.to_account_info(),
system_program: ctx.system_program.to_account_info(),
};
let cpi_ctx = CpiContext::new_with_signer(
ctx.openbook_v2_program.to_account_info(),
cpi_accounts,
seeds,
);
openbook_v2::cpi::settle_funds(cpi_ctx)
}

View File

@ -0,0 +1,607 @@
use crate::accounts_zerocopy::AccountInfoRef;
use crate::error::*;
use crate::health::*;
use crate::i80f48::ClampToInt;
use crate::instructions::{apply_vault_difference, OODifference};
use crate::logs::{emit_stack, OpenbookV2OpenOrdersBalanceLog};
use crate::serum3_cpi::{OpenOrdersAmounts, OpenOrdersSlim};
use crate::state::*;
use crate::util::clock_now;
use anchor_lang::prelude::*;
use fixed::types::I80F48;
use openbook_v2::cpi::Return;
use openbook_v2::state::OpenOrdersAccount;
use openbook_v2::state::{
Order as OpenbookV2Order, PlaceOrderType as OpenbookV2OrderType, Side as OpenbookV2Side,
MAX_OPEN_ORDERS,
};
use crate::accounts_ix::*;
pub fn openbook_v2_place_order(
ctx: Context<OpenbookV2PlaceOrder>,
order: OpenbookV2Order,
limit: u8,
) -> Result<()> {
require_gte!(order.max_base_lots, 0);
require_gte!(order.max_quote_lots_including_fees, 0);
let openbook_market = ctx.accounts.openbook_v2_market.load()?;
require!(
!openbook_market.is_reduce_only(),
MangoError::MarketInReduceOnlyMode
);
let receiver_token_index = match order.side {
OpenbookV2Side::Bid => openbook_market.base_token_index,
OpenbookV2Side::Ask => openbook_market.quote_token_index,
};
let payer_token_index = match order.side {
OpenbookV2Side::Bid => openbook_market.quote_token_index,
OpenbookV2Side::Ask => openbook_market.base_token_index,
};
//
// Validation
//
{
let account = ctx.accounts.account.load_full()?;
// account constraint #1
require!(
account
.fixed
.is_owner_or_delegate(ctx.accounts.authority.key()),
MangoError::SomeError
);
// Validate open_orders #2
require!(
account
.openbook_v2_orders(openbook_market.market_index)?
.open_orders
== ctx.accounts.open_orders.key(),
MangoError::SomeError
);
}
// Validate bank and vault #3
let group_key = ctx.accounts.group.key();
let mut account = ctx.accounts.account.load_full_mut()?;
let (now_ts, now_slot) = clock_now();
let retriever = new_fixed_order_account_retriever_with_optional_banks(
ctx.remaining_accounts,
&account.borrow(),
now_slot,
)?;
let (_, _, payer_active_index) = account.ensure_token_position(payer_token_index)?;
let (_, _, receiver_active_index) = account.ensure_token_position(receiver_token_index)?;
// This verifies that the required banks are available and that their oracles are valid
let (payer_bank, payer_bank_oracle) =
retriever.bank_and_oracle(&group_key, payer_active_index, payer_token_index)?;
let (receiver_bank, receiver_bank_oracle) =
retriever.bank_and_oracle(&group_key, receiver_active_index, receiver_token_index)?;
require_keys_eq!(payer_bank.vault, ctx.accounts.payer_vault.key());
// Validate bank token indexes #4
require_eq!(
ctx.accounts.payer_bank.load()?.token_index,
payer_token_index
);
require_eq!(
ctx.accounts.receiver_bank.load()?.token_index,
receiver_token_index
);
//
// Pre-health computation
//
let mut health_cache = new_health_cache_skipping_missing_banks_and_bad_oracles(
&account.borrow(),
&retriever,
now_ts,
)
.context("pre init health")?;
// The payer and receiver token banks/oracles must be passed and be valid
health_cache.token_info_index(payer_token_index)?;
health_cache.token_info_index(receiver_token_index)?;
let pre_health_opt = if !account.fixed.is_in_health_region() {
let pre_init_health = account.check_health_pre(&health_cache)?;
Some(pre_init_health)
} else {
None
};
drop(retriever);
// No version check required, bank writable from v1
//
// Before-order tracking
//
let base_lot_size: u64;
let quote_lot_size: u64;
{
let openbook_market_external = ctx.accounts.openbook_v2_market_external.load()?;
base_lot_size = openbook_market_external.base_lot_size.try_into().unwrap();
quote_lot_size = openbook_market_external.quote_lot_size.try_into().unwrap();
}
let before_vault = ctx.accounts.payer_vault.amount;
let before_oo_free_slots;
let before_had_bids;
let before_had_asks;
let before_oo = {
let open_orders = ctx.accounts.open_orders.load()?;
before_oo_free_slots = MAX_OPEN_ORDERS - open_orders.all_orders_in_use().count();
before_had_bids = open_orders.position.bids_base_lots != 0;
before_had_asks = open_orders.position.asks_base_lots != 0;
OpenOrdersSlim::from_oo_v2(&open_orders, base_lot_size, quote_lot_size)
};
// Provide a readable error message in case the vault doesn't have enough tokens
let max_base_lots: u64 = order.max_base_lots.try_into().unwrap();
let max_quote_lots: u64 = order.max_quote_lots_including_fees.try_into().unwrap();
let needed_amount = match order.side {
OpenbookV2Side::Ask => {
(max_base_lots * base_lot_size).saturating_sub(before_oo.native_base_free())
}
OpenbookV2Side::Bid => {
(max_quote_lots * quote_lot_size).saturating_sub(before_oo.native_quote_free())
}
};
if before_vault < needed_amount {
return err!(MangoError::InsufficentBankVaultFunds).with_context(|| {
format!(
"bank vault does not have enough tokens, need {} but have {}",
needed_amount, before_vault
)
});
}
// Get price lots before the book gets modified
let price_lots;
{
let bids = ctx.accounts.bids.load_mut()?;
let asks = ctx.accounts.asks.load_mut()?;
let order_book = openbook_v2::state::Orderbook { bids, asks };
price_lots = order.price(now_ts, None, &order_book)?.0;
}
//
// CPI to place order
//
let group = ctx.accounts.group.load()?;
let group_seeds = group_seeds!(group);
cpi_place_order(ctx.accounts, &[group_seeds], &order, price_lots, limit)?;
//
// After-order tracking
//
let open_orders = ctx.accounts.open_orders.load()?;
let after_oo_free_slots = MAX_OPEN_ORDERS - open_orders.all_orders_in_use().count();
let after_oo = OpenOrdersSlim::from_oo_v2(&open_orders, base_lot_size, quote_lot_size);
let oo_difference = OODifference::new(&before_oo, &after_oo);
//
// Track the highest bid and lowest ask, to be able to evaluate worst-case health even
// when they cross the oracle
//
let openbook = account.openbook_v2_orders_mut(openbook_market.market_index)?;
if !before_had_bids {
// The 0 state means uninitialized/no value
openbook.highest_placed_bid_inv = 0.0;
openbook.lowest_placed_bid_inv = 0.0
}
if !before_had_asks {
openbook.lowest_placed_ask = 0.0;
openbook.highest_placed_ask = 0.0;
}
// in the normal quote per base units
let limit_price = price_lots as f64 * quote_lot_size as f64 / base_lot_size as f64;
let new_order_on_book = after_oo_free_slots != before_oo_free_slots;
if new_order_on_book {
match order.side {
OpenbookV2Side::Ask => {
openbook.lowest_placed_ask = if openbook.lowest_placed_ask == 0.0 {
limit_price
} else {
openbook.lowest_placed_ask.min(limit_price)
};
openbook.highest_placed_ask = if openbook.highest_placed_ask == 0.0 {
limit_price
} else {
openbook.highest_placed_ask.max(limit_price)
}
}
OpenbookV2Side::Bid => {
// in base per quote units, to avoid a division in health
let limit_price_inv = 1.0 / limit_price;
openbook.highest_placed_bid_inv = if openbook.highest_placed_bid_inv == 0.0 {
limit_price_inv
} else {
// the highest bid has the lowest _inv value
openbook.highest_placed_bid_inv.min(limit_price_inv)
};
openbook.lowest_placed_bid_inv = if openbook.lowest_placed_bid_inv == 0.0 {
limit_price_inv
} else {
// lowest bid has max _inv value
openbook.lowest_placed_bid_inv.max(limit_price_inv)
}
}
}
}
emit_stack(OpenbookV2OpenOrdersBalanceLog {
mango_group: ctx.accounts.group.key(),
mango_account: ctx.accounts.account.key(),
market_index: openbook_market.market_index,
base_token_index: openbook_market.base_token_index,
quote_token_index: openbook_market.quote_token_index,
base_total: after_oo.native_base_total(),
base_free: after_oo.native_base_free(),
quote_total: after_oo.native_quote_total(),
quote_free: after_oo.native_quote_free(),
referrer_rebates_accrued: after_oo.native_rebates(),
});
ctx.accounts.payer_vault.reload()?;
let after_vault = ctx.accounts.payer_vault.amount;
// Placing an order cannot increase vault balance
require_gte!(before_vault, after_vault);
let before_position_native;
let vault_difference;
{
let mut payer_bank = ctx.accounts.payer_bank.load_mut()?;
let mut receiver_bank = ctx.accounts.receiver_bank.load_mut()?;
let (base_bank, quote_bank) = match order.side {
OpenbookV2Side::Bid => (&mut receiver_bank, &mut payer_bank),
OpenbookV2Side::Ask => (&mut payer_bank, &mut receiver_bank),
};
update_bank_potential_tokens(openbook, base_bank, quote_bank, &after_oo);
// Track position before withdraw happens
before_position_native = account
.token_position_mut(payer_bank.token_index)?
.0
.native(&payer_bank);
// Charge the difference in vault balance to the user's account
vault_difference = {
apply_vault_difference(
ctx.accounts.account.key(),
&mut account.borrow_mut(),
SpotMarketIndex::OpenbookV2(openbook_market.market_index),
&mut payer_bank,
after_vault,
before_vault,
)?
};
}
// Deposit limit check, receiver side:
// Placing an order can always increase the receiver bank deposits on fill.
{
let receiver_bank = ctx.accounts.receiver_bank.load()?;
receiver_bank
.check_deposit_and_oo_limit()
.with_context(|| std::format!("on {}", receiver_bank.name()))?;
}
// Payer bank safety checks like reduce-only, net borrows, vault-to-deposits ratio
let withdrawn_from_vault = I80F48::from(before_vault - after_vault);
let payer_bank = ctx.accounts.payer_bank.load()?;
if withdrawn_from_vault > before_position_native {
require_msg_typed!(
!payer_bank.are_borrows_reduce_only(),
MangoError::TokenInReduceOnlyMode,
"the payer tokens cannot be borrowed"
);
payer_bank.enforce_max_utilization_on_borrow()?;
payer_bank.check_net_borrows(payer_bank_oracle)?;
// Deposit limit check, payer side:
// The payer bank deposits could increase when cancelling the order later:
// Imagine the account borrowing payer tokens to place the order, repaying the borrows
// and then cancelling the order to create a deposit.
//
// However, if the account only decreases its deposits to place an order it can't
// worsen the situation and should always go through, even if payer deposit limits are
// already exceeded.
payer_bank
.check_deposit_and_oo_limit()
.with_context(|| std::format!("on {}", payer_bank.name()))?;
} else {
payer_bank.enforce_borrows_lte_deposits()?;
}
// Limit order price bands: If the order ends up on the book, ensure
// - a bid isn't too far below oracle
// - an ask isn't too far above oracle
// because placing orders that are guaranteed to never be hit can be bothersome:
// For example placing a very large bid near zero would make the potential_base_tokens
// value go through the roof, reducing available init margin for other users.
let band_threshold = openbook_market.oracle_price_band();
if new_order_on_book && band_threshold != f32::MAX {
let (base_oracle, quote_oracle) = match order.side {
OpenbookV2Side::Bid => (&receiver_bank_oracle, &payer_bank_oracle),
OpenbookV2Side::Ask => (&payer_bank_oracle, &receiver_bank_oracle),
};
let base_oracle_f64 = base_oracle.to_num::<f64>();
let quote_oracle_f64 = quote_oracle.to_num::<f64>();
// this has the same units as base_oracle: USD per BASE; limit_price is in QUOTE per BASE
let limit_price_in_dollar = limit_price * quote_oracle_f64;
let band_factor = 1.0 + band_threshold as f64;
match order.side {
OpenbookV2Side::Bid => {
require_msg_typed!(
limit_price_in_dollar * band_factor >= base_oracle_f64,
MangoError::SpotPriceBandExceeded,
"bid price {} must be larger than {} ({}% of oracle)",
limit_price,
base_oracle_f64 / (quote_oracle_f64 * band_factor),
(100.0 / band_factor) as u64,
);
}
OpenbookV2Side::Ask => {
require_msg_typed!(
limit_price_in_dollar <= base_oracle_f64 * band_factor,
MangoError::SpotPriceBandExceeded,
"ask price {} must be smaller than {} ({}% of oracle)",
limit_price,
base_oracle_f64 * band_factor / quote_oracle_f64,
(100.0 * band_factor) as u64,
);
}
}
}
// Health cache updates for the changed account state
let receiver_bank = ctx.accounts.receiver_bank.load()?;
let payer_bank = ctx.accounts.payer_bank.load()?;
// update scaled weights for receiver bank
health_cache.adjust_token_balance(&receiver_bank, I80F48::ZERO)?;
vault_difference.adjust_health_cache_token_balance(&mut health_cache, &payer_bank)?;
let openbook_account = account.openbook_v2_orders(openbook_market.market_index)?;
oo_difference.recompute_health_cache_openbook_v2_state(
&mut health_cache,
&openbook_account,
&open_orders,
)?;
// Check the receiver's reduce only flag.
//
// Note that all orders on the book executing can still cause a net deposit. That's because
// the total spot potential amount assumes all reserved amounts convert at the current
// oracle price.
//
// This also requires that all spot oos that touch the receiver_token are avaliable in the
// health cache. We make this a general requirement to avoid surprises.
health_cache.check_has_all_spot_infos_for_token(&account.borrow(), receiver_token_index)?;
if receiver_bank.are_deposits_reduce_only() {
let balance = health_cache.token_info(receiver_token_index)?.balance_spot;
let potential =
health_cache.total_spot_potential(HealthType::Maint, receiver_token_index)?;
require_msg_typed!(
balance + potential < 1,
MangoError::TokenInReduceOnlyMode,
"receiver bank does not accept deposits"
);
}
//
// Health check
//
if let Some(pre_init_health) = pre_health_opt {
account.check_health_post(&health_cache, pre_init_health)?;
}
Ok(())
}
/// Uses the changes in OpenOrders and vaults to adjust the user token position,
/// collect fees and optionally adjusts the HealthCache.
pub fn apply_settle_changes(
group: &Group,
account_pk: Pubkey,
account: &mut MangoAccountRefMut,
base_bank: &mut Bank,
quote_bank: &mut Bank,
openbook_market: &OpenbookV2Market,
before_base_vault: u64,
before_quote_vault: u64,
before_oo: &OpenOrdersSlim,
after_base_vault: u64,
after_quote_vault: u64,
after_oo: &OpenOrdersSlim,
health_cache: Option<&mut HealthCache>,
fees_to_dao: bool,
quote_oracle: Option<&AccountInfo>,
open_orders: &OpenOrdersAccount,
) -> Result<()> {
let mut received_fees = 0;
if fees_to_dao {
// Example: rebates go from 100 -> 10. That means we credit 90 in fees.
received_fees = before_oo
.native_rebates()
.saturating_sub(after_oo.native_rebates());
quote_bank.collected_fees_native += I80F48::from(received_fees);
// Credit the buyback_fees at the current value of the quote token.
if let Some(quote_oracle_ai) = quote_oracle {
let clock = Clock::get()?;
let now_ts = clock.unix_timestamp.try_into().unwrap();
let quote_oracle_ref = &AccountInfoRef::borrow(quote_oracle_ai)?;
let quote_oracle_price = quote_bank.oracle_price(
&OracleAccountInfos::from_reader(quote_oracle_ref),
Some(clock.slot),
)?;
let quote_asset_price = quote_oracle_price.min(quote_bank.stable_price());
account
.fixed
.expire_buyback_fees(now_ts, group.buyback_fees_expiry_interval);
let fees_in_usd = I80F48::from(received_fees) * quote_asset_price;
account
.fixed
.accrue_buyback_fees(fees_in_usd.clamp_to_u64());
}
}
// Don't count the referrer rebate fees as part of the vault change that should be
// credited to the user.
let after_quote_vault_adjusted = after_quote_vault - received_fees;
// Settle cannot decrease vault balances
require_gte!(after_base_vault, before_base_vault);
require_gte!(after_quote_vault_adjusted, before_quote_vault);
// Credit the difference in vault balances to the user's account
let base_difference = apply_vault_difference(
account_pk,
account,
SpotMarketIndex::OpenbookV2(openbook_market.market_index),
base_bank,
after_base_vault,
before_base_vault,
)?;
let quote_difference = apply_vault_difference(
account_pk,
account,
SpotMarketIndex::OpenbookV2(openbook_market.market_index),
quote_bank,
after_quote_vault_adjusted,
before_quote_vault,
)?;
// Tokens were moved from open orders into banks again: also update the tracking
// for potential_serum_tokens on the banks.
{
let openbook_orders = account.openbook_v2_orders_mut(openbook_market.market_index)?;
update_bank_potential_tokens(openbook_orders, base_bank, quote_bank, after_oo);
}
if let Some(health_cache) = health_cache {
base_difference.adjust_health_cache_token_balance(health_cache, &base_bank)?;
quote_difference.adjust_health_cache_token_balance(health_cache, &quote_bank)?;
let serum_account = account.openbook_v2_orders(openbook_market.market_index)?;
OODifference::new(&before_oo, &after_oo).recompute_health_cache_openbook_v2_state(
health_cache,
serum_account,
open_orders,
)?;
}
Ok(())
}
fn update_bank_potential_tokens(
openbook_orders: &mut OpenbookV2Orders,
base_bank: &mut Bank,
quote_bank: &mut Bank,
oo: &OpenOrdersSlim,
) {
assert_eq!(openbook_orders.base_token_index, base_bank.token_index);
assert_eq!(openbook_orders.quote_token_index, quote_bank.token_index);
// Potential tokens are all tokens on the side, plus reserved on the other side
// converted at favorable price. This creates an overestimation of the potential
// base and quote tokens flowing out of this open orders account.
let new_base = oo.native_base_total()
+ (oo.native_quote_reserved() as f64 * openbook_orders.lowest_placed_bid_inv) as u64;
let new_quote = oo.native_quote_total()
+ (oo.native_base_reserved() as f64 * openbook_orders.highest_placed_ask) as u64;
let old_base = openbook_orders.potential_base_tokens;
let old_quote = openbook_orders.potential_quote_tokens;
base_bank.update_potential_openbook_tokens(old_base, new_base);
quote_bank.update_potential_openbook_tokens(old_quote, new_quote);
openbook_orders.potential_base_tokens = new_base;
openbook_orders.potential_quote_tokens = new_quote;
}
fn cpi_place_order(
ctx: &OpenbookV2PlaceOrder,
seeds: &[&[&[u8]]],
order: &OpenbookV2Order,
price_lots: i64,
limit: u8,
) -> Result<Return<Option<u128>>> {
let cpi_accounts = openbook_v2::cpi::accounts::PlaceOrder {
signer: ctx.group.to_account_info(),
open_orders_account: ctx.open_orders.to_account_info(),
open_orders_admin: None,
user_token_account: ctx.payer_vault.to_account_info(),
market: ctx.openbook_v2_market_external.to_account_info(),
bids: ctx.bids.to_account_info(),
asks: ctx.asks.to_account_info(),
event_heap: ctx.event_heap.to_account_info(),
market_vault: ctx.market_vault.to_account_info(),
oracle_a: None, // we don't yet support markets with oracles
oracle_b: None,
token_program: ctx.token_program.to_account_info(),
};
let cpi_ctx = CpiContext::new_with_signer(
ctx.openbook_v2_program.to_account_info(),
cpi_accounts,
seeds,
);
let expiry_timestamp: u64 = if order.time_in_force > 0 {
Clock::get()
.unwrap()
.unix_timestamp
.saturating_add(order.time_in_force as i64)
.try_into()
.unwrap()
} else {
0
};
let order_type = match order.params {
openbook_v2::state::OrderParams::Market => OpenbookV2OrderType::Market,
openbook_v2::state::OrderParams::ImmediateOrCancel { price_lots } => {
OpenbookV2OrderType::ImmediateOrCancel
}
openbook_v2::state::OrderParams::Fixed {
price_lots,
order_type,
} => match order_type {
openbook_v2::state::PostOrderType::Limit => OpenbookV2OrderType::Limit,
openbook_v2::state::PostOrderType::PostOnly => OpenbookV2OrderType::PostOnly,
openbook_v2::state::PostOrderType::PostOnlySlide => OpenbookV2OrderType::PostOnlySlide,
},
openbook_v2::state::OrderParams::OraclePegged {
price_offset_lots,
order_type,
peg_limit,
} => todo!(),
};
let args = openbook_v2::PlaceOrderArgs {
side: order.side,
price_lots,
max_base_lots: order.max_base_lots,
max_quote_lots_including_fees: order.max_quote_lots_including_fees,
client_order_id: order.client_order_id,
order_type,
expiry_timestamp,
self_trade_behavior: order.self_trade_behavior,
limit,
};
msg!("args {:?}", args);
openbook_v2::cpi::place_order(cpi_ctx, args)
}

View File

@ -0,0 +1,95 @@
use anchor_lang::prelude::*;
use crate::error::*;
use crate::state::*;
use crate::util::fill_from_str;
use crate::accounts_ix::*;
use crate::logs::{emit_stack, OpenbookV2RegisterMarketLog};
pub fn openbook_v2_register_market(
ctx: Context<OpenbookV2RegisterMarket>,
market_index: OpenbookV2MarketIndex,
name: String,
oracle_price_band: f32,
) -> Result<()> {
let is_fast_listing;
let group = ctx.accounts.group.load()?;
// checking the admin account (#1)
if ctx.accounts.admin.key() == group.admin {
is_fast_listing = false;
} else if ctx.accounts.admin.key() == group.fast_listing_admin {
is_fast_listing = true;
} else {
return Err(error_msg!(
"admin must be the group admin or group fast listing admin"
));
}
let base_bank = ctx.accounts.base_bank.load()?;
let quote_bank = ctx.accounts.quote_bank.load()?;
let market_external = ctx.accounts.openbook_v2_market_external.load()?;
require_keys_eq!(
market_external.quote_mint,
quote_bank.mint,
MangoError::SomeError
);
require_keys_eq!(
market_external.base_mint,
base_bank.mint,
MangoError::SomeError
);
if is_fast_listing {
// C tier tokens (no borrows, no asset weight) allow wider bands if the quote token has
// no deposit limits
let base_c_tier =
base_bank.are_borrows_reduce_only() && base_bank.maint_asset_weight.is_zero();
let quote_has_no_deposit_limit = quote_bank.deposit_weight_scale_start_quote == f64::MAX
&& quote_bank.deposit_limit == 0;
if base_c_tier && quote_has_no_deposit_limit {
require_eq!(oracle_price_band, 19.0);
} else {
require_eq!(oracle_price_band, 1.0);
}
}
let mut openbook_market = ctx.accounts.openbook_v2_market.load_init()?;
*openbook_market = OpenbookV2Market {
group: ctx.accounts.group.key(),
base_token_index: base_bank.token_index,
quote_token_index: quote_bank.token_index,
reduce_only: 0,
force_close: 0,
name: fill_from_str(&name)?,
openbook_v2_program: ctx.accounts.openbook_v2_program.key(),
openbook_v2_market_external: ctx.accounts.openbook_v2_market_external.key(),
market_index,
bump: *ctx
.bumps
.get("openbook_v2_market")
.ok_or(MangoError::SomeError)?,
oracle_price_band,
registration_time: Clock::get()?.unix_timestamp.try_into().unwrap(),
reserved: [0; 1027],
};
let mut openbook_index_reservation = ctx.accounts.index_reservation.load_init()?;
*openbook_index_reservation = OpenbookV2MarketIndexReservation {
group: ctx.accounts.group.key(),
market_index,
reserved: [0; 38],
};
emit_stack(OpenbookV2RegisterMarketLog {
mango_group: ctx.accounts.group.key(),
openbook_market: ctx.accounts.openbook_v2_market.key(),
market_index,
base_token_index: base_bank.token_index,
quote_token_index: quote_bank.token_index,
openbook_program: ctx.accounts.openbook_v2_program.key(),
openbook_market_external: ctx.accounts.openbook_v2_market_external.key(),
});
Ok(())
}

View File

@ -0,0 +1,295 @@
use anchor_lang::prelude::*;
use fixed::types::I80F48;
use crate::error::*;
use crate::serum3_cpi::{OpenOrdersAmounts, OpenOrdersSlim};
use crate::state::*;
use openbook_v2::cpi::accounts::SettleFunds;
use crate::accounts_ix::*;
use crate::instructions::openbook_v2_place_order::apply_settle_changes;
use crate::logs::{
emit_stack, LoanOriginationFeeInstruction, OpenbookV2OpenOrdersBalanceLog, WithdrawLoanLog,
};
use crate::accounts_zerocopy::AccountInfoRef;
/// Settling means moving free funds from the open orders account
/// back into the mango account wallet.
///
/// There will be free funds on open_orders when an order was triggered.
///
pub fn openbook_v2_settle_funds<'info>(
ctx: Context<OpenbookV2SettleFunds>,
fees_to_dao: bool,
) -> Result<()> {
let openbook_market = ctx.accounts.openbook_v2_market.load()?;
//
// Validation
//
{
let account = ctx.accounts.account.load_full()?;
// account constraint #1
require!(
account
.fixed
.is_owner_or_delegate(ctx.accounts.authority.key()),
MangoError::SomeError
);
// Validate open_orders #2
require!(
account
.openbook_v2_orders(openbook_market.market_index)?
.open_orders
== ctx.accounts.open_orders.key(),
MangoError::SomeError
);
// Validate banks and vaults #3
let quote_bank = ctx.accounts.quote_bank.load()?;
require!(
quote_bank.vault == ctx.accounts.quote_vault.key(),
MangoError::SomeError
);
require!(
quote_bank.token_index == openbook_market.quote_token_index,
MangoError::SomeError
);
let base_bank = ctx.accounts.base_bank.load()?;
require!(
base_bank.vault == ctx.accounts.base_vault.key(),
MangoError::SomeError
);
require!(
base_bank.token_index == openbook_market.base_token_index,
MangoError::SomeError
);
// Validate oracles #4
require_keys_eq!(
base_bank.oracle,
ctx.accounts.base_oracle.key(),
MangoError::SomeError
);
require_keys_eq!(
quote_bank.oracle,
ctx.accounts.quote_oracle.key(),
MangoError::SomeError
);
}
//
// Charge any open loan origination fees
//
let base_lot_size: u64;
let quote_lot_size: u64;
let before_oo;
{
let openbook_market_external = ctx.accounts.openbook_v2_market_external.load()?;
base_lot_size = openbook_market_external.base_lot_size.try_into().unwrap();
quote_lot_size = openbook_market_external.quote_lot_size.try_into().unwrap();
let open_orders = ctx.accounts.open_orders.load()?;
before_oo = OpenOrdersSlim::from_oo_v2(&open_orders, base_lot_size, quote_lot_size);
let mut account = ctx.accounts.account.load_full_mut()?;
let mut base_bank = ctx.accounts.base_bank.load_mut()?;
let mut quote_bank = ctx.accounts.quote_bank.load_mut()?;
charge_loan_origination_fees(
&ctx.accounts.group.key(),
&ctx.accounts.account.key(),
openbook_market.market_index,
&mut base_bank,
&mut quote_bank,
&mut account.borrow_mut(),
&before_oo,
Some(&ctx.accounts.base_oracle.to_account_info()),
Some(&ctx.accounts.quote_oracle.to_account_info()),
)?;
}
//
// Settle
//
let before_base_vault = ctx.accounts.base_vault.amount;
let before_quote_vault = ctx.accounts.quote_vault.amount;
let mango_account_seeds_data = ctx.accounts.account.load()?.pda_seeds();
let seeds = &mango_account_seeds_data.signer_seeds();
cpi_settle_funds(ctx.accounts, &[seeds])?;
//
// After-settle tracking
//
let after_oo = {
let open_orders = ctx.accounts.open_orders.load()?;
OpenOrdersSlim::from_oo_v2(&open_orders, base_lot_size, quote_lot_size)
};
ctx.accounts.base_vault.reload()?;
ctx.accounts.quote_vault.reload()?;
let after_base_vault = ctx.accounts.base_vault.amount;
let after_quote_vault = ctx.accounts.quote_vault.amount;
let mut account = ctx.accounts.account.load_full_mut()?;
let mut base_bank = ctx.accounts.base_bank.load_mut()?;
let mut quote_bank = ctx.accounts.quote_bank.load_mut()?;
let group = ctx.accounts.group.load()?;
let open_orders = ctx.accounts.open_orders.load()?;
apply_settle_changes(
&group,
ctx.accounts.account.key(),
&mut account.borrow_mut(),
&mut base_bank,
&mut quote_bank,
&openbook_market,
before_base_vault,
before_quote_vault,
&before_oo,
after_base_vault,
after_quote_vault,
&after_oo,
None,
fees_to_dao,
Some(&ctx.accounts.quote_oracle.to_account_info()),
&open_orders,
)?;
emit_stack(OpenbookV2OpenOrdersBalanceLog {
mango_group: ctx.accounts.group.key(),
mango_account: ctx.accounts.account.key(),
market_index: openbook_market.market_index,
base_token_index: openbook_market.base_token_index,
quote_token_index: openbook_market.quote_token_index,
base_total: after_oo.native_base_total(),
base_free: after_oo.native_base_free(),
quote_total: after_oo.native_quote_total(),
quote_free: after_oo.native_quote_free(),
referrer_rebates_accrued: after_oo.native_rebates(),
});
Ok(())
}
// Charge fees if the potential borrows are bigger than the funds on the open orders account
pub fn charge_loan_origination_fees(
group_pubkey: &Pubkey,
account_pubkey: &Pubkey,
market_index: OpenbookV2MarketIndex,
base_bank: &mut Bank,
quote_bank: &mut Bank,
account: &mut MangoAccountRefMut,
before_oo: &OpenOrdersSlim,
base_oracle: Option<&AccountInfo>,
quote_oracle: Option<&AccountInfo>,
) -> Result<()> {
let openbook_v2_orders = account.openbook_v2_orders_mut(market_index).unwrap();
let now_ts = Clock::get()?.unix_timestamp.try_into().unwrap();
let oo_base_total = before_oo.native_base_total();
let actualized_base_loan = I80F48::from_num(
openbook_v2_orders
.base_borrows_without_fee
.saturating_sub(oo_base_total),
);
if actualized_base_loan > 0 {
openbook_v2_orders.base_borrows_without_fee = oo_base_total;
// now that the loan is actually materialized, charge the loan origination fee
// note: the withdraw has already happened while placing the order
let base_token_account = account.token_position_mut(base_bank.token_index)?.0;
let withdraw_result = base_bank.withdraw_loan_origination_fee(
base_token_account,
actualized_base_loan,
now_ts,
)?;
let base_oracle_price = base_oracle
.map(|ai| {
let ai_ref = &AccountInfoRef::borrow(ai)?;
base_bank.oracle_price(
&OracleAccountInfos::from_reader(ai_ref),
Some(Clock::get()?.slot),
)
})
.transpose()?;
emit_stack(WithdrawLoanLog {
mango_group: *group_pubkey,
mango_account: *account_pubkey,
token_index: base_bank.token_index,
loan_amount: withdraw_result.loan_amount.to_bits(),
loan_origination_fee: withdraw_result.loan_origination_fee.to_bits(),
instruction: LoanOriginationFeeInstruction::OpenbookV2SettleFunds,
price: base_oracle_price.map(|p| p.to_bits()),
});
}
let openbook_v2_account = account.openbook_v2_orders_mut(market_index).unwrap();
let oo_quote_total = before_oo.native_quote_total();
let actualized_quote_loan = I80F48::from_num::<u64>(
openbook_v2_account
.quote_borrows_without_fee
.saturating_sub(oo_quote_total),
);
if actualized_quote_loan > 0 {
openbook_v2_account.quote_borrows_without_fee = oo_quote_total;
// now that the loan is actually materialized, charge the loan origination fee
// note: the withdraw has already happened while placing the order
let quote_token_account = account.token_position_mut(quote_bank.token_index)?.0;
let withdraw_result = quote_bank.withdraw_loan_origination_fee(
quote_token_account,
actualized_quote_loan,
now_ts,
)?;
let quote_oracle_price = quote_oracle
.map(|ai| {
let ai_ref = &AccountInfoRef::borrow(ai)?;
quote_bank.oracle_price(
&OracleAccountInfos::from_reader(ai_ref),
Some(Clock::get()?.slot),
)
})
.transpose()?;
emit_stack(WithdrawLoanLog {
mango_group: *group_pubkey,
mango_account: *account_pubkey,
token_index: quote_bank.token_index,
loan_amount: withdraw_result.loan_amount.to_bits(),
loan_origination_fee: withdraw_result.loan_origination_fee.to_bits(),
instruction: LoanOriginationFeeInstruction::OpenbookV2SettleFunds,
price: quote_oracle_price.map(|p| p.to_bits()),
});
}
Ok(())
}
fn cpi_settle_funds<'info>(ctx: &OpenbookV2SettleFunds<'info>, seeds: &[&[&[u8]]]) -> Result<()> {
let cpi_accounts = SettleFunds {
penalty_payer: ctx.authority.to_account_info(),
market: ctx.openbook_v2_market_external.to_account_info(),
market_authority: ctx.market_vault_signer.to_account_info(),
market_base_vault: ctx.market_base_vault.to_account_info(),
market_quote_vault: ctx.market_quote_vault.to_account_info(),
user_base_account: ctx.base_vault.to_account_info(),
user_quote_account: ctx.quote_vault.to_account_info(),
referrer_account: Some(ctx.quote_vault.to_account_info()),
token_program: ctx.token_program.to_account_info(),
owner: ctx.account.to_account_info(),
open_orders_account: ctx.open_orders.to_account_info(),
system_program: ctx.system_program.to_account_info(),
};
let cpi_ctx = CpiContext::new_with_signer(
ctx.openbook_v2_program.to_account_info(),
cpi_accounts,
seeds,
);
openbook_v2::cpi::settle_funds(cpi_ctx)
}

View File

@ -3,8 +3,8 @@ use anchor_lang::prelude::*;
use crate::accounts_ix::*;
use crate::error::*;
use crate::health::*;
use crate::instructions::apply_settle_changes;
use crate::instructions::charge_loan_origination_fees;
use crate::instructions::serum3_place_order::apply_settle_changes;
use crate::instructions::serum3_settle_funds::charge_loan_origination_fees;
use crate::logs::{emit_stack, Serum3OpenOrdersBalanceLogV2};
use crate::serum3_cpi::{load_open_orders_ref, OpenOrdersAmounts, OpenOrdersSlim};
use crate::state::*;

View File

@ -324,7 +324,7 @@ pub fn serum3_place_order(
apply_vault_difference(
ctx.accounts.account.key(),
&mut account.borrow_mut(),
serum_market.market_index,
SpotMarketIndex::Serum3(serum_market.market_index),
&mut payer_bank,
after_vault,
before_vault,
@ -390,7 +390,7 @@ pub fn serum3_place_order(
Serum3Side::Bid => {
require_msg_typed!(
limit_price_in_dollar * band_factor >= base_oracle_f64,
MangoError::Serum3PriceBandExceeded,
MangoError::SpotPriceBandExceeded,
"bid price {} must be larger than {} ({}% of oracle)",
limit_price,
base_oracle_f64 / (quote_oracle_f64 * band_factor),
@ -400,7 +400,7 @@ pub fn serum3_place_order(
Serum3Side::Ask => {
require_msg_typed!(
limit_price_in_dollar <= base_oracle_f64 * band_factor,
MangoError::Serum3PriceBandExceeded,
MangoError::SpotPriceBandExceeded,
"ask price {} must be smaller than {} ({}% of oracle)",
limit_price,
base_oracle_f64 * band_factor / quote_oracle_f64,
@ -425,26 +425,16 @@ pub fn serum3_place_order(
// Check the receiver's reduce only flag.
//
// Note that all orders on the book executing can still cause a net deposit. That's because
// the total serum3 potential amount assumes all reserved amounts convert at the current
// the total spot potential amount assumes all reserved amounts convert at the current
// oracle price.
//
// This also requires that all serum3 oos that touch the receiver_token are avaliable in the
// This also requires that all spot oos that touch the receiver_token are avaliable in the
// health cache. We make this a general requirement to avoid surprises.
for serum3 in account.active_serum3_orders() {
if serum3.base_token_index == receiver_token_index
|| serum3.quote_token_index == receiver_token_index
{
require_msg!(
health_cache.serum3_infos.iter().any(|s3| s3.market_index == serum3.market_index),
"health cache is missing serum3 info {} involving receiver token {}; passed banks and oracles?",
serum3.market_index, receiver_token_index
);
}
}
health_cache.check_has_all_spot_infos_for_token(&account.borrow(), receiver_token_index)?;
if receiver_bank_reduce_only {
let balance = health_cache.token_info(receiver_token_index)?.balance_spot;
let potential =
health_cache.total_serum3_potential(HealthType::Maint, receiver_token_index)?;
health_cache.total_spot_potential(HealthType::Maint, receiver_token_index)?;
require_msg_typed!(
balance + potential < 1,
MangoError::TokenInReduceOnlyMode,
@ -490,6 +480,20 @@ impl OODifference {
self.free_quote_change,
)
}
pub fn recompute_health_cache_openbook_v2_state(
&self,
health_cache: &mut HealthCache,
openbook_account: &OpenbookV2Orders,
open_orders: &openbook_v2::state::OpenOrdersAccount,
) -> Result<()> {
health_cache.recompute_openbook_v2_info(
openbook_account,
open_orders,
self.free_base_change,
self.free_quote_change,
)
}
}
pub struct VaultDifference {
@ -512,10 +516,10 @@ impl VaultDifference {
/// Called in apply_settle_changes() and place_order to adjust token positions after
/// changing the vault balances
/// Also logs changes to token balances
fn apply_vault_difference(
pub fn apply_vault_difference(
account_pk: Pubkey,
account: &mut MangoAccountRefMut,
serum_market_index: Serum3MarketIndex,
spot_market_index: SpotMarketIndex,
bank: &mut Bank,
vault_after: u64,
vault_before: u64,
@ -540,16 +544,32 @@ fn apply_vault_difference(
.to_num::<u64>();
let indexed_position = position.indexed_position;
let market = account.serum3_orders_mut(serum_market_index).unwrap();
let borrows_without_fee;
if bank.token_index == market.base_token_index {
borrows_without_fee = &mut market.base_borrows_without_fee;
} else if bank.token_index == market.quote_token_index {
borrows_without_fee = &mut market.quote_borrows_without_fee;
} else {
return Err(error_msg!(
"assert failed: apply_vault_difference called with bad token index"
));
match spot_market_index {
SpotMarketIndex::Serum3(index) => {
let market = account.serum3_orders_mut(index).unwrap();
if bank.token_index == market.base_token_index {
borrows_without_fee = &mut market.base_borrows_without_fee;
} else if bank.token_index == market.quote_token_index {
borrows_without_fee = &mut market.quote_borrows_without_fee;
} else {
return Err(error_msg!(
"assert failed: apply_vault_difference called with bad token index"
));
};
}
SpotMarketIndex::OpenbookV2(index) => {
let market = account.openbook_v2_orders_mut(index).unwrap();
if bank.token_index == market.base_token_index {
borrows_without_fee = &mut market.base_borrows_without_fee;
} else if bank.token_index == market.quote_token_index {
borrows_without_fee = &mut market.quote_borrows_without_fee;
} else {
return Err(error_msg!(
"assert failed: apply_vault_difference called with bad token index"
));
};
}
};
// Only for place: Add to potential borrow amount
@ -635,7 +655,7 @@ pub fn apply_settle_changes(
let base_difference = apply_vault_difference(
account_pk,
account,
serum_market.market_index,
SpotMarketIndex::Serum3(serum_market.market_index),
base_bank,
after_base_vault,
before_base_vault,
@ -643,7 +663,7 @@ pub fn apply_settle_changes(
let quote_difference = apply_vault_difference(
account_pk,
account,
serum_market.market_index,
SpotMarketIndex::Serum3(serum_market.market_index),
quote_bank,
after_quote_vault_adjusted,
before_quote_vault,

View File

@ -5,8 +5,8 @@ use crate::error::*;
use crate::serum3_cpi::{load_open_orders_ref, OpenOrdersAmounts, OpenOrdersSlim};
use crate::state::*;
use super::apply_settle_changes;
use crate::accounts_ix::*;
use crate::instructions::serum3_place_order::apply_settle_changes;
use crate::logs::{
emit_stack, LoanOriginationFeeInstruction, Serum3OpenOrdersBalanceLogV2, WithdrawLoanLog,
};

View File

@ -52,9 +52,9 @@ pub fn token_charge_collateral_fees(ctx: Context<TokenChargeCollateralFees>) ->
// pretend all spot orders are closed and settled and add their funds back to
// the token positions.
let mut token_balances = health_cache.effective_token_balances(HealthType::Maint);
for s3info in health_cache.serum3_infos.iter() {
token_balances[s3info.base_info_index].spot_and_perp += s3info.reserved_base;
token_balances[s3info.quote_info_index].spot_and_perp += s3info.reserved_quote;
for spot_info in health_cache.spot_infos.iter() {
token_balances[spot_info.base_info_index].spot_and_perp += spot_info.reserved_base;
token_balances[spot_info.quote_info_index].spot_and_perp += spot_info.reserved_quote;
}
let mut total_liab_health = I80F48::ZERO;

View File

@ -731,7 +731,7 @@ mod tests {
liqee_buffer.extend_from_slice(&[0u8; 512]);
let mut liqee = MangoAccountValue::from_bytes(&liqee_buffer).unwrap();
{
liqee.resize_dynamic_content(3, 5, 4, 6, 1).unwrap();
liqee.resize_dynamic_content(3, 5, 4, 6, 1, 0).unwrap();
liqee.ensure_token_position(0).unwrap();
liqee.ensure_token_position(1).unwrap();
}

View File

@ -114,6 +114,7 @@ pub fn token_register(
interest_target_utilization,
interest_curve_scaling: interest_curve_scaling.into(),
potential_serum_tokens: 0,
potential_openbook_tokens: 0,
maint_weight_shift_start: 0,
maint_weight_shift_end: 0,
maint_weight_shift_duration_inv: I80F48::ZERO,
@ -126,7 +127,8 @@ pub fn token_register(
collected_liquidation_fees: I80F48::ZERO,
collected_collateral_fees: I80F48::ZERO,
collateral_fee_per_day,
reserved: [0; 1900],
padding2: [0; 4],
reserved: [0; 1888],
};
let oracle_ref = &AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?;

View File

@ -100,6 +100,7 @@ pub fn token_register_trustless(
interest_target_utilization: 0.5,
interest_curve_scaling: 4.0,
potential_serum_tokens: 0,
potential_openbook_tokens: 0,
maint_weight_shift_start: 0,
maint_weight_shift_end: 0,
maint_weight_shift_duration_inv: I80F48::ZERO,
@ -111,7 +112,8 @@ pub fn token_register_trustless(
collected_liquidation_fees: I80F48::ZERO,
collected_collateral_fees: I80F48::ZERO,
collateral_fee_per_day: 0.0, // TODO
reserved: [0; 1900],
padding2: [0; 4],
reserved: [0; 1888],
};
let oracle_ref = &AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?;
if let Ok(oracle_price) = bank.oracle_price(&OracleAccountInfos::from_reader(oracle_ref), None)

View File

@ -355,6 +355,7 @@ pub mod mango_v4 {
perp_count,
perp_oo_count,
0,
0,
name,
)?;
Ok(())
@ -382,6 +383,36 @@ pub mod mango_v4 {
perp_count,
perp_oo_count,
token_conditional_swap_count,
0,
name,
)?;
Ok(())
}
pub fn account_create_v3(
ctx: Context<AccountCreateV3>,
account_num: u32,
token_count: u8,
serum3_count: u8,
perp_count: u8,
perp_oo_count: u8,
token_conditional_swap_count: u8,
openbook_v2_count: u8,
name: String,
) -> Result<()> {
#[cfg(feature = "enable-gpl")]
instructions::account_create(
&ctx.accounts.account,
*ctx.bumps.get("account").ok_or(MangoError::SomeError)?,
ctx.accounts.group.key(),
ctx.accounts.owner.key(),
account_num,
token_count,
serum3_count,
perp_count,
perp_oo_count,
token_conditional_swap_count,
openbook_v2_count,
name,
)?;
Ok(())
@ -395,7 +426,15 @@ pub mod mango_v4 {
perp_oo_count: u8,
) -> Result<()> {
#[cfg(feature = "enable-gpl")]
instructions::account_expand(ctx, token_count, serum3_count, perp_count, perp_oo_count, 0)?;
instructions::account_expand(
ctx,
token_count,
serum3_count,
perp_count,
perp_oo_count,
0,
0,
)?;
Ok(())
}
@ -415,6 +454,29 @@ pub mod mango_v4 {
perp_count,
perp_oo_count,
token_conditional_swap_count,
0,
)?;
Ok(())
}
pub fn account_expand_v3(
ctx: Context<AccountExpand>,
token_count: u8,
serum3_count: u8,
perp_count: u8,
perp_oo_count: u8,
token_conditional_swap_count: u8,
openbook_v2_count: u8,
) -> Result<()> {
#[cfg(feature = "enable-gpl")]
instructions::account_expand(
ctx,
token_count,
serum3_count,
perp_count,
perp_oo_count,
token_conditional_swap_count,
openbook_v2_count,
)?;
Ok(())
}
@ -1682,7 +1744,10 @@ pub mod mango_v4 {
ctx: Context<OpenbookV2RegisterMarket>,
market_index: OpenbookV2MarketIndex,
name: String,
oracle_price_band: f32,
) -> Result<()> {
#[cfg(feature = "enable-gpl")]
instructions::openbook_v2_register_market(ctx, market_index, name, oracle_price_band)?;
Ok(())
}
@ -1690,59 +1755,91 @@ pub mod mango_v4 {
ctx: Context<OpenbookV2EditMarket>,
reduce_only_opt: Option<bool>,
force_close_opt: Option<bool>,
name_opt: Option<String>,
oracle_price_band_opt: Option<f32>,
) -> Result<()> {
#[cfg(feature = "enable-gpl")]
instructions::openbook_v2_edit_market(
ctx,
reduce_only_opt,
force_close_opt,
name_opt,
oracle_price_band_opt,
)?;
Ok(())
}
pub fn openbook_v2_deregister_market(ctx: Context<OpenbookV2DeregisterMarket>) -> Result<()> {
#[cfg(feature = "enable-gpl")]
instructions::openbook_v2_deregister_market(ctx)?;
Ok(())
}
pub fn openbook_v2_create_open_orders(
ctx: Context<OpenbookV2CreateOpenOrders>,
account_num: u32,
) -> Result<()> {
pub fn openbook_v2_create_open_orders(ctx: Context<OpenbookV2CreateOpenOrders>) -> Result<()> {
#[cfg(feature = "enable-gpl")]
instructions::openbook_v2_create_open_orders(ctx)?;
Ok(())
}
pub fn openbook_v2_close_open_orders(ctx: Context<OpenbookV2CloseOpenOrders>) -> Result<()> {
#[cfg(feature = "enable-gpl")]
instructions::openbook_v2_close_open_orders(ctx)?;
Ok(())
}
#[allow(clippy::too_many_arguments)]
pub fn openbook_v2_place_order(
ctx: Context<OpenbookV2PlaceOrder>,
side: u8, // openbook_v2::state::Side
limit_price: u64,
max_base_qty: u64,
max_native_quote_qty_including_fees: u64,
self_trade_behavior: u8, // openbook_v2::state::SelfTradeBehavior
order_type: u8, // openbook_v2::state::PlaceOrderType
side: OpenbookV2Side,
price_lots: i64,
max_base_lots: i64,
max_quote_lots_including_fees: i64,
client_order_id: u64,
limit: u16,
order_type: OpenbookV2PlaceOrderType,
self_trade_behavior: OpenbookV2SelfTradeBehavior,
reduce_only: bool,
expiry_timestamp: u64,
limit: u8,
) -> Result<()> {
Ok(())
}
use openbook_v2::state::{Order, OrderParams};
let time_in_force = match Order::tif_from_expiry(expiry_timestamp) {
Some(t) => t,
None => {
msg!("Order is already expired");
return Ok(());
}
};
let order = Order {
side: side.to_external(),
max_base_lots,
max_quote_lots_including_fees,
client_order_id,
time_in_force,
self_trade_behavior: self_trade_behavior.to_external(),
params: match order_type {
OpenbookV2PlaceOrderType::Market => OrderParams::Market {},
OpenbookV2PlaceOrderType::ImmediateOrCancel => {
OrderParams::ImmediateOrCancel { price_lots }
}
_ => OrderParams::Fixed {
price_lots,
order_type: order_type.to_external_post_order_type()?,
},
},
};
#[allow(clippy::too_many_arguments)]
pub fn openbook_v2_place_taker_order(
ctx: Context<OpenbookV2PlaceTakeOrder>,
side: u8, // openbook_v2::state::Side
limit_price: u64,
max_base_qty: u64,
max_native_quote_qty_including_fees: u64,
self_trade_behavior: u8, // openbook_v2::state::SelfTradeBehavior
client_order_id: u64,
limit: u16,
) -> Result<()> {
#[cfg(feature = "enable-gpl")]
instructions::openbook_v2_place_order(ctx, order, limit)?;
Ok(())
}
pub fn openbook_v2_cancel_order(
ctx: Context<OpenbookV2CancelOrder>,
side: u8, // openbook_v2::state::Side
side: OpenbookV2Side,
order_id: u128,
) -> Result<()> {
#[cfg(feature = "enable-gpl")]
instructions::openbook_v2_cancel_order(ctx, side.to_external(), order_id)?;
Ok(())
}
@ -1750,6 +1847,8 @@ pub mod mango_v4 {
ctx: Context<OpenbookV2SettleFunds>,
fees_to_dao: bool,
) -> Result<()> {
#[cfg(feature = "enable-gpl")]
instructions::openbook_v2_settle_funds(ctx, fees_to_dao)?;
Ok(())
}
@ -1757,13 +1856,25 @@ pub mod mango_v4 {
ctx: Context<OpenbookV2LiqForceCancelOrders>,
limit: u8,
) -> Result<()> {
#[cfg(feature = "enable-gpl")]
instructions::openbook_v2_liq_force_cancel_orders(ctx, limit)?;
Ok(())
}
pub fn openbook_v2_cancel_all_orders(
ctx: Context<OpenbookV2CancelOrder>,
limit: u8,
side_opt: Option<OpenbookV2Side>,
) -> Result<()> {
#[cfg(feature = "enable-gpl")]
instructions::openbook_v2_cancel_all_orders(
ctx,
limit,
match side_opt {
Some(side) => Some(side.to_external()),
None => None,
},
)?;
Ok(())
}

View File

@ -387,6 +387,20 @@ pub struct Serum3OpenOrdersBalanceLogV2 {
pub referrer_rebates_accrued: u64,
}
#[event]
pub struct OpenbookV2OpenOrdersBalanceLog {
pub mango_group: Pubkey,
pub mango_account: Pubkey,
pub market_index: u16,
pub base_token_index: u16,
pub quote_token_index: u16,
pub base_total: u64,
pub base_free: u64,
pub quote_total: u64,
pub quote_free: u64,
pub referrer_rebates_accrued: u64,
}
#[derive(PartialEq, Copy, Clone, Debug, AnchorSerialize, AnchorDeserialize)]
#[repr(u8)]
pub enum LoanOriginationFeeInstruction {
@ -398,6 +412,9 @@ pub enum LoanOriginationFeeInstruction {
Serum3SettleFunds,
TokenWithdraw,
TokenConditionalSwapTrigger,
OpenbookV2LiqForceCancelOrders,
OpenbookV2PlaceOrder,
OpenbookV2SettleFunds,
}
#[event]
@ -499,6 +516,17 @@ pub struct Serum3RegisterMarketLog {
pub serum_program_external: Pubkey,
}
#[event]
pub struct OpenbookV2RegisterMarketLog {
pub mango_group: Pubkey,
pub openbook_market: Pubkey,
pub market_index: u16,
pub base_token_index: u16,
pub quote_token_index: u16,
pub openbook_program: Pubkey,
pub openbook_market_external: Pubkey,
}
#[event]
pub struct PerpLiqBaseOrPositivePnlLog {
pub mango_group: Pubkey,

View File

@ -1,4 +1,5 @@
use anchor_lang::prelude::*;
use openbook_v2::state::OpenOrdersAccount as OpenOrdersV2;
use serum_dex::state::{OpenOrders, ToAlignedBytes, ACCOUNT_HEAD_PADDING};
use std::cell::{Ref, RefMut};
@ -128,7 +129,7 @@ pub fn load_open_orders(acc: &impl AccountReader) -> Result<&serum_dex::state::O
}
pub fn load_open_orders_bytes(bytes: &[u8]) -> Result<&serum_dex::state::OpenOrders> {
Ok(bytemuck::from_bytes(strip_dex_padding(bytes)?))
Ok(bytemuck::try_from_bytes(strip_dex_padding(bytes)?).map_err(|_| MangoError::SomeError)?)
}
pub fn pubkey_from_u64_array(d: [u64; 4]) -> Pubkey {
@ -155,6 +156,22 @@ impl OpenOrdersSlim {
referrer_rebates_accrued: oo.referrer_rebates_accrued,
}
}
pub fn from_oo_v2(oo: &OpenOrdersV2, base_lot_size: u64, quote_lot_size: u64) -> Self {
let bids_quote_lots: u64 = oo.position.bids_quote_lots.try_into().unwrap();
let asks_base_lots: u64 = oo.position.asks_base_lots.try_into().unwrap();
let base_locked_native = asks_base_lots * base_lot_size;
let quote_locked_native = bids_quote_lots * quote_lot_size;
Self {
native_coin_free: oo.position.base_free_native,
native_coin_total: base_locked_native + oo.position.base_free_native,
native_pc_free: oo.position.quote_free_native,
native_pc_total: quote_locked_native
+ oo.position.quote_free_native
+ oo.position.locked_maker_fees,
referrer_rebates_accrued: oo.position.referrer_rebates_available,
}
}
}
pub trait OpenOrdersAmounts {

View File

@ -186,7 +186,7 @@ pub struct Bank {
/// Except when first migrating to having this field, then 0.0
pub interest_curve_scaling: f64,
/// Largest amount of tokens that might be added the the bank based on
/// Largest amount of tokens that might be added the bank based on
/// serum open order execution.
pub potential_serum_tokens: u64,
@ -232,7 +232,14 @@ pub struct Bank {
pub collateral_fee_per_day: f32,
#[derivative(Debug = "ignore")]
pub reserved: [u8; 1900],
pub padding2: [u8; 4],
/// Largest amount of tokens that might be added the bank based on
/// oenbook open order execution.
pub potential_openbook_tokens: u64,
#[derivative(Debug = "ignore")]
pub reserved: [u8; 1888],
}
const_assert_eq!(
size_of::<Bank>(),
@ -270,8 +277,9 @@ const_assert_eq!(
+ 32
+ 8
+ 16 * 4
+ 4
+ 1900
+ 4 * 2
+ 8
+ 1888
);
const_assert_eq!(size_of::<Bank>(), 3064);
const_assert_eq!(size_of::<Bank>() % 8, 0);
@ -322,6 +330,7 @@ impl Bank {
flash_loan_token_account_initial: u64::MAX,
net_borrows_in_window: 0,
potential_serum_tokens: 0,
potential_openbook_tokens: 0,
bump,
bank_num,
@ -382,7 +391,8 @@ impl Bank {
zero_util_rate: existing_bank.zero_util_rate,
platform_liquidation_fee: existing_bank.platform_liquidation_fee,
collateral_fee_per_day: existing_bank.collateral_fee_per_day,
reserved: [0; 1900],
padding2: [0; 4],
reserved: [0; 1888],
}
}
@ -961,7 +971,8 @@ impl Bank {
let deposits = self.deposit_index * (self.indexed_deposits + I80F48::DELTA);
let serum = I80F48::from(self.potential_serum_tokens);
let total = deposits + serum;
let openbook = I80F48::from(self.potential_openbook_tokens);
let total = deposits + serum + openbook;
I80F48::from(self.deposit_limit) - total
}
@ -976,17 +987,19 @@ impl Bank {
// will not cause a limit overrun.
let deposits = self.native_deposits();
let serum = I80F48::from(self.potential_serum_tokens);
let total = deposits + serum;
let openbook = I80F48::from(self.potential_openbook_tokens);
let total = deposits + serum + openbook;
let remaining = I80F48::from(self.deposit_limit) - total;
if remaining < 0 {
return Err(error_msg_typed!(
MangoError::BankDepositLimit,
"deposit limit exceeded: remaining: {}, total: {}, limit: {}, deposits: {}, serum: {}",
"deposit limit exceeded: remaining: {}, total: {}, limit: {}, deposits: {}, serum: {}, openbook: {}",
remaining,
total,
self.deposit_limit,
deposits,
serum,
openbook,
));
}
@ -1242,8 +1255,9 @@ impl Bank {
if self.deposit_weight_scale_start_quote == f64::MAX {
return self.init_asset_weight;
}
let all_deposits =
self.native_deposits().to_num::<f64>() + self.potential_serum_tokens as f64;
let all_deposits = self.native_deposits().to_num::<f64>()
+ self.potential_serum_tokens as f64
+ self.potential_openbook_tokens as f64;
let deposits_quote = all_deposits * price.to_num::<f64>();
if deposits_quote <= self.deposit_weight_scale_start_quote {
self.init_asset_weight
@ -1282,6 +1296,17 @@ impl Bank {
self.potential_serum_tokens = self.potential_serum_tokens.saturating_sub(old - new);
}
}
/// Grows potential_openbook_tokens if new > old, shrinks it otherwise
#[inline(always)]
pub fn update_potential_openbook_tokens(&mut self, old: u64, new: u64) {
if new >= old {
self.potential_openbook_tokens += new - old;
} else {
self.potential_openbook_tokens =
self.potential_openbook_tokens.saturating_sub(old - new);
}
}
}
#[macro_export]
@ -1579,7 +1604,8 @@ mod tests {
bank.net_borrow_limit_per_window_quote = 100;
bank.net_borrows_in_window = 200;
bank.deposit_limit = 100;
bank.potential_serum_tokens = 200;
bank.potential_serum_tokens = 100;
bank.potential_openbook_tokens = 100;
let half = I80F48::from(50);
bank.checked_transfer_with_fee(&mut a1, half, &mut a2, half, 0, I80F48::ONE)

View File

@ -147,10 +147,6 @@ impl Group {
pub fn is_ix_enabled(&self, ix: IxGate) -> bool {
self.ix_gate & (1 << ix as u128) == 0
}
pub fn openbook_v2_supported(&self) -> bool {
self.is_testing()
}
}
/// Enum for lookup into ix gate
@ -243,7 +239,8 @@ pub enum IxGate {
TokenForceWithdraw = 72,
SequenceCheck = 73,
HealthCheck = 74,
GroupChangeInsuranceFund = 75,
OpenbookV2CancelAllOrders = 75,
GroupChangeInsuranceFund = 76,
// NOTE: Adding new variants requires matching changes in ts and the ix_gate_set instruction.
}

View File

@ -16,7 +16,6 @@ use crate::health::{HealthCache, HealthType};
use crate::logs::{emit_stack, DeactivatePerpPositionLog, DeactivateTokenPositionLog};
use crate::util;
use super::BookSideOrderTree;
use super::FillEvent;
use super::LeafNode;
use super::PerpMarket;
@ -27,6 +26,7 @@ use super::TokenConditionalSwap;
use super::TokenIndex;
use super::FREE_ORDER_SLOT;
use super::{dynamic_account::*, Group};
use super::{BookSideOrderTree, OpenbookV2MarketIndex, OpenbookV2Orders};
use super::{PerpPosition, Serum3Orders, TokenPosition};
use super::{Side, SideAndOrderTree};
@ -34,7 +34,7 @@ type BorshVecLength = u32;
const BORSH_VEC_PADDING_BYTES: usize = 4;
const BORSH_VEC_SIZE_BYTES: usize = 4;
const DEFAULT_MANGO_ACCOUNT_VERSION: u8 = 1;
const DYNAMIC_RESERVED_BYTES: usize = 64;
const DYNAMIC_RESERVED_BYTES: usize = 56;
// Return variants for check_liquidatable method, should be wrapped in a Result
// for a future possiblity of returning any error
@ -183,9 +183,12 @@ pub struct MangoAccount {
#[derivative(Debug = "ignore")]
pub padding8: u32,
pub token_conditional_swaps: Vec<TokenConditionalSwap>,
#[derivative(Debug = "ignore")]
pub padding9: u32,
pub openbook_v2: Vec<OpenbookV2Orders>,
#[derivative(Debug = "ignore")]
pub reserved_dynamic: [u8; 64],
pub reserved_dynamic: [u8; 56],
}
impl MangoAccount {
@ -224,7 +227,9 @@ impl MangoAccount {
perp_open_orders: vec![PerpOpenOrder::default(); 6],
padding8: Default::default(),
token_conditional_swaps: vec![TokenConditionalSwap::default(); 2],
reserved_dynamic: [0; 64],
padding9: Default::default(),
openbook_v2: vec![OpenbookV2Orders::default(); 5],
reserved_dynamic: [0; 56],
}
}
@ -235,6 +240,7 @@ impl MangoAccount {
perp_count: u8,
perp_oo_count: u8,
token_conditional_swap_count: u8,
openbook_v2_count: u8,
) -> usize {
8 + size_of::<MangoAccountFixed>()
+ Self::dynamic_size(
@ -243,6 +249,7 @@ impl MangoAccount {
perp_count,
perp_oo_count,
token_conditional_swap_count,
openbook_v2_count,
)
}
@ -280,7 +287,7 @@ impl MangoAccount {
+ BORSH_VEC_PADDING_BYTES
}
pub fn dynamic_reserved_bytes_offset(
pub fn dynamic_openbook_v2_vec_offset(
token_count: u8,
serum3_count: u8,
perp_count: u8,
@ -294,6 +301,24 @@ impl MangoAccount {
perp_oo_count,
) + (BORSH_VEC_SIZE_BYTES
+ size_of::<TokenConditionalSwap>() * usize::from(token_conditional_swap_count))
+ BORSH_VEC_PADDING_BYTES
}
pub fn dynamic_reserved_bytes_offset(
token_count: u8,
serum3_count: u8,
perp_count: u8,
perp_oo_count: u8,
token_conditional_swap_count: u8,
openbook_v2_count: u8,
) -> usize {
Self::dynamic_openbook_v2_vec_offset(
token_count,
serum3_count,
perp_count,
perp_oo_count,
token_conditional_swap_count,
) + (BORSH_VEC_SIZE_BYTES + size_of::<OpenbookV2Orders>() * usize::from(openbook_v2_count))
}
pub fn dynamic_size(
@ -302,6 +327,7 @@ impl MangoAccount {
perp_count: u8,
perp_oo_count: u8,
token_conditional_swap_count: u8,
openbook_v2_count: u8,
) -> usize {
Self::dynamic_reserved_bytes_offset(
token_count,
@ -309,6 +335,7 @@ impl MangoAccount {
perp_count,
perp_oo_count,
token_conditional_swap_count,
openbook_v2_count,
) + DYNAMIC_RESERVED_BYTES
}
}
@ -472,6 +499,7 @@ pub struct MangoAccountDynamicHeader {
pub perp_count: u8,
pub perp_oo_count: u8,
pub token_conditional_swap_count: u8,
pub openbook_v2_count: u8,
}
impl DynamicHeader for MangoAccountDynamicHeader {
@ -515,18 +543,27 @@ impl DynamicHeader for MangoAccountDynamicHeader {
perp_count,
perp_oo_count,
);
let token_conditional_swap_count = if dynamic_data.len()
> token_conditional_swap_vec_offset + BORSH_VEC_SIZE_BYTES
{
let token_conditional_swap_count =
u8::try_from(BorshVecLength::from_le_bytes(*array_ref![
dynamic_data,
token_conditional_swap_vec_offset,
BORSH_VEC_SIZE_BYTES
]))
.unwrap()
} else {
0
};
.unwrap();
let openbook_v2_vec_offset = MangoAccount::dynamic_openbook_v2_vec_offset(
token_count,
serum3_count,
perp_count,
perp_oo_count,
token_conditional_swap_count,
);
let openbook_v2_count = u8::try_from(BorshVecLength::from_le_bytes(*array_ref![
dynamic_data,
openbook_v2_vec_offset,
BORSH_VEC_SIZE_BYTES
]))
.unwrap();
Ok(Self {
token_count,
@ -534,6 +571,7 @@ impl DynamicHeader for MangoAccountDynamicHeader {
perp_count,
perp_oo_count,
token_conditional_swap_count,
openbook_v2_count,
})
}
_ => err!(MangoError::NotImplementedError).context("unexpected header version number"),
@ -563,6 +601,7 @@ impl MangoAccountDynamicHeader {
self.perp_count,
self.perp_oo_count,
self.token_conditional_swap_count,
self.openbook_v2_count,
)
}
@ -608,6 +647,17 @@ impl MangoAccountDynamicHeader {
+ raw_index * size_of::<TokenConditionalSwap>()
}
fn openbook_v2_offset(&self, raw_index: usize) -> usize {
MangoAccount::dynamic_openbook_v2_vec_offset(
self.token_count,
self.serum3_count,
self.perp_count,
self.perp_oo_count,
self.token_conditional_swap_count,
) + BORSH_VEC_SIZE_BYTES
+ raw_index * size_of::<OpenbookV2Orders>()
}
fn reserved_bytes_offset(&self) -> usize {
MangoAccount::dynamic_reserved_bytes_offset(
self.token_count,
@ -615,6 +665,7 @@ impl MangoAccountDynamicHeader {
self.perp_count,
self.perp_oo_count,
self.token_conditional_swap_count,
self.openbook_v2_count,
)
}
@ -633,6 +684,9 @@ impl MangoAccountDynamicHeader {
pub fn token_conditional_swap_count(&self) -> usize {
self.token_conditional_swap_count.into()
}
pub fn openbook_v2_count(&self) -> usize {
self.openbook_v2_count.into()
}
pub fn zero() -> Self {
Self {
@ -641,11 +695,15 @@ impl MangoAccountDynamicHeader {
perp_count: 0,
perp_oo_count: 0,
token_conditional_swap_count: 0,
openbook_v2_count: 0,
}
}
pub fn expected_health_accounts(&self) -> usize {
self.token_count() * 2 + self.serum3_count() + self.perp_count() * 2
self.token_count() * 2
+ self.serum3_count()
+ self.perp_count() * 2
+ self.openbook_v2_count()
}
pub fn max_health_accounts() -> usize {
@ -921,6 +979,42 @@ impl<
.ok_or_else(|| error_msg!("no free token conditional swap index"))
}
pub fn openbook_v2_orders(
&self,
market_index: OpenbookV2MarketIndex,
) -> Result<&OpenbookV2Orders> {
self.all_openbook_v2_orders()
.find(|p| p.is_active_for_market(market_index))
.ok_or_else(|| {
error_msg!(
"openbook v2 orders for market index {} not found",
market_index
)
})
}
pub(crate) fn openbook_v2_orders_by_raw_index_unchecked(
&self,
raw_index: usize,
) -> &OpenbookV2Orders {
get_helper(self.dynamic(), self.header().openbook_v2_offset(raw_index))
}
pub fn openbook_v2_orders_by_raw_index(&self, raw_index: usize) -> Result<&OpenbookV2Orders> {
require_gt!(self.header().openbook_v2_count(), raw_index);
Ok(self.openbook_v2_orders_by_raw_index_unchecked(raw_index))
}
pub fn all_openbook_v2_orders(&self) -> impl Iterator<Item = &OpenbookV2Orders> + '_ {
(0..self.header().openbook_v2_count())
.map(|i| self.openbook_v2_orders_by_raw_index_unchecked(i))
}
pub fn active_openbook_v2_orders(&self) -> impl Iterator<Item = &OpenbookV2Orders> + '_ {
self.all_openbook_v2_orders()
.filter(|openbook_v2_order| openbook_v2_order.is_active())
}
pub fn borrow(&self) -> MangoAccountRef {
MangoAccountRef {
header: self.header(),
@ -1121,6 +1215,67 @@ impl<
.ok_or_else(|| error_msg!("serum3 orders for market index {} not found", market_index))
}
// get mut OpenbookV2Orders at raw_index
pub fn openbook_v2_orders_mut_by_raw_index(
&mut self,
raw_index: usize,
) -> &mut OpenbookV2Orders {
let offset = self.header().openbook_v2_offset(raw_index);
get_helper_mut(self.dynamic_mut(), offset)
}
pub fn create_openbook_v2_orders(
&mut self,
market_index: OpenbookV2MarketIndex,
) -> Result<&mut OpenbookV2Orders> {
if self.openbook_v2_orders(market_index).is_ok() {
return err!(MangoError::OpenbookV2OpenOrdersExistAlready);
}
let raw_index_opt = self.all_openbook_v2_orders().position(|p| !p.is_active());
if let Some(raw_index) = raw_index_opt {
*(self.openbook_v2_orders_mut_by_raw_index(raw_index)) = OpenbookV2Orders {
market_index: market_index as OpenbookV2MarketIndex,
..OpenbookV2Orders::default()
};
Ok(self.openbook_v2_orders_mut_by_raw_index(raw_index))
} else {
err!(MangoError::NoFreeOpenbookV2OpenOrdersIndex)
}
}
pub fn deactivate_openbook_v2_orders(
&mut self,
market_index: OpenbookV2MarketIndex,
) -> Result<()> {
let raw_index = self
.all_openbook_v2_orders()
.position(|p| p.is_active_for_market(market_index))
.ok_or_else(|| {
error_msg!("openbook v2 open orders index {} not found", market_index)
})?;
self.openbook_v2_orders_mut_by_raw_index(raw_index)
.market_index = OpenbookV2MarketIndex::MAX;
Ok(())
}
pub fn openbook_v2_orders_mut(
&mut self,
market_index: OpenbookV2MarketIndex,
) -> Result<&mut OpenbookV2Orders> {
let raw_index_opt = self
.all_openbook_v2_orders()
.position(|p| p.is_active_for_market(market_index));
raw_index_opt
.map(|raw_index| self.openbook_v2_orders_mut_by_raw_index(raw_index))
.ok_or_else(|| {
error_msg!(
"openbook v2 orders for market index {} not found",
market_index
)
})
}
// get mut PerpPosition at raw_index
pub fn perp_position_mut_by_raw_index(&mut self, raw_index: usize) -> &mut PerpPosition {
let offset = self.header().perp_offset(raw_index);
@ -1529,6 +1684,12 @@ impl<
self.write_borsh_vec_length_and_padding(offset, count)
}
fn write_openbook_v2_length(&mut self) {
let offset = self.header().openbook_v2_offset(0);
let count = self.header().openbook_v2_count;
self.write_borsh_vec_length_and_padding(offset, count)
}
pub fn resize_dynamic_content(
&mut self,
new_token_count: u8,
@ -1536,6 +1697,7 @@ impl<
new_perp_count: u8,
new_perp_oo_count: u8,
new_token_conditional_swap_count: u8,
new_openbook_v2_count: u8,
) -> Result<()> {
let new_header = MangoAccountDynamicHeader {
token_count: new_token_count,
@ -1543,6 +1705,7 @@ impl<
perp_count: new_perp_count,
perp_oo_count: new_perp_oo_count,
token_conditional_swap_count: new_token_conditional_swap_count,
openbook_v2_count: new_openbook_v2_count,
};
let old_header = self.header().clone();
@ -1663,12 +1826,33 @@ impl<
active_tcs += 1;
}
let mut active_openbook_v2_orders = 0;
for i in 0..old_header.openbook_v2_count() {
let src = old_header.openbook_v2_offset(i);
let pos: &OpenbookV2Orders = get_helper(dynamic, src);
if !pos.is_active() {
continue;
}
if i != active_openbook_v2_orders {
let dst = old_header.openbook_v2_offset(active_openbook_v2_orders);
unsafe {
sol_memmove(
&mut dynamic[dst],
&mut dynamic[src],
size_of::<OpenbookV2Orders>(),
);
}
}
active_openbook_v2_orders += 1;
}
// Check that the new allocations can fit the existing data
require_gte!(new_header.token_count(), active_token_positions);
require_gte!(new_header.serum3_count(), active_serum3_orders);
require_gte!(new_header.perp_count(), active_perp_positions);
require_gte!(new_header.perp_oo_count(), blocked_perp_oo);
require_gte!(new_header.token_conditional_swap_count(), active_tcs);
require_gte!(new_header.openbook_v2_count(), active_openbook_v2_orders);
// First move pass: go left-to-right and move any blocks that need to be moved
// to the left. This will never overwrite other data, because:
@ -1726,6 +1910,18 @@ impl<
);
}
}
let old_openbook_v2_start = old_header.openbook_v2_offset(0);
let new_openbook_v2_start = new_header.openbook_v2_offset(0);
if new_openbook_v2_start < old_openbook_v2_start && active_openbook_v2_orders > 0 {
unsafe {
sol_memmove(
&mut dynamic[new_openbook_v2_start],
&mut dynamic[old_openbook_v2_start],
size_of::<OpenbookV2Orders>() * active_openbook_v2_orders,
);
}
}
}
// Second move pass: Go right-to-left and move everything to the right if needed.
@ -1735,6 +1931,18 @@ impl<
// - if the block to the right was moved to the left, we know that its start will
// be >= our block's end
{
let old_openbook_v2_start = old_header.openbook_v2_offset(0);
let new_openbook_v2_start = new_header.openbook_v2_offset(0);
if new_openbook_v2_start > old_openbook_v2_start && active_openbook_v2_orders > 0 {
unsafe {
sol_memmove(
&mut dynamic[new_openbook_v2_start],
&mut dynamic[old_openbook_v2_start],
size_of::<OpenbookV2Orders>() * active_openbook_v2_orders,
);
}
}
let old_tcs_start = old_header.token_conditional_swap_offset(0);
let new_tcs_start = new_header.token_conditional_swap_offset(0);
if new_tcs_start > old_tcs_start && active_tcs > 0 {
@ -1804,6 +2012,10 @@ impl<
*get_helper_mut(dynamic, new_header.token_conditional_swap_offset(i)) =
TokenConditionalSwap::default();
}
for i in active_openbook_v2_orders..new_header.openbook_v2_count() {
*get_helper_mut(dynamic, new_header.openbook_v2_offset(i)) =
OpenbookV2Orders::default();
}
}
{
let offset = new_header.reserved_bytes_offset();
@ -1820,6 +2032,7 @@ impl<
self.write_perp_length();
self.write_perp_oo_length();
self.write_token_conditional_swap_length();
self.write_openbook_v2_length();
Ok(())
}
@ -1905,7 +2118,9 @@ mod tests {
account.perps.len() as u8,
account.perp_open_orders.len() as u8,
account.token_conditional_swaps.len() as u8,
account.openbook_v2.len() as u8,
);
assert_eq!(expected_space, 8 + bytes.len());
MangoAccountValue::from_bytes(&bytes).unwrap()
@ -1940,7 +2155,10 @@ mod tests {
account.token_conditional_swaps[0].buy_token_index = 14;
let account_bytes = AnchorSerialize::try_to_vec(&account).unwrap();
assert_eq!(8 + account_bytes.len(), MangoAccount::space(8, 8, 4, 8, 12));
assert_eq!(
8 + account_bytes.len(),
MangoAccount::space(8, 8, 4, 8, 12, 5)
);
let account2 = MangoAccountValue::from_bytes(&account_bytes).unwrap();
assert_eq!(account.group, account2.fixed.group);
@ -2286,6 +2504,62 @@ mod tests {
assert_eq!(tcs.id, 123); // old data
}
#[test]
fn test_openbook_v2_orders() {
let mut account = make_test_account();
assert!(account.openbook_v2_orders(1).is_err());
assert!(account.openbook_v2_orders_mut(3).is_err());
// When we make the test account we zero init the dynamic section.
// This would never happen outside of tests. If it did we would incorrectly think the orders slot is active.
// assert_eq!(
// account.openbook_v2_orders_by_raw_index_unchecked(0).market_index,
// OpenbookV2MarketIndex::MAX
// );
assert_eq!(
account.create_openbook_v2_orders(1).unwrap().market_index,
1
);
assert_eq!(
account.create_openbook_v2_orders(7).unwrap().market_index,
7
);
assert_eq!(
account.create_openbook_v2_orders(42).unwrap().market_index,
42
);
assert!(account.create_openbook_v2_orders(7).is_err());
assert_eq!(account.active_openbook_v2_orders().count(), 3);
assert!(account.deactivate_openbook_v2_orders(7).is_ok());
assert_eq!(
account
.openbook_v2_orders_by_raw_index_unchecked(1)
.market_index,
OpenbookV2MarketIndex::MAX
);
assert!(account.create_openbook_v2_orders(8).is_ok());
assert_eq!(
account
.openbook_v2_orders_by_raw_index_unchecked(1)
.market_index,
8
);
assert_eq!(account.active_openbook_v2_orders().count(), 3);
assert!(account.deactivate_openbook_v2_orders(1).is_ok());
assert!(account.openbook_v2_orders(1).is_err());
assert!(account.openbook_v2_orders_mut(1).is_err());
assert!(account.openbook_v2_orders(8).is_ok());
assert!(account.openbook_v2_orders(42).is_ok());
assert_eq!(account.active_openbook_v2_orders().count(), 2);
assert_eq!(account.openbook_v2_orders_mut(42).unwrap().market_index, 42);
assert_eq!(account.openbook_v2_orders_mut(8).unwrap().market_index, 8);
assert!(account.openbook_v2_orders_mut(7).is_err());
}
fn make_resize_test_account(header: &MangoAccountDynamicHeader) -> MangoAccountValue {
let mut account = MangoAccount::default_for_tests();
account
@ -2300,6 +2574,9 @@ mod tests {
account
.perp_open_orders
.resize(header.perp_oo_count(), PerpOpenOrder::default());
account
.openbook_v2
.resize(header.openbook_v2_count(), OpenbookV2Orders::default());
let mut bytes = AnchorSerialize::try_to_vec(&account).unwrap();
// The MangoAccount struct is missing some dynamic fields, add space for them
@ -2310,14 +2587,23 @@ mod tests {
let (fixed, dynamic) = bytes.split_at_mut(size_of::<MangoAccountFixed>());
let mut out_header = MangoAccountDynamicHeader::from_bytes(dynamic).unwrap();
out_header.token_conditional_swap_count = header.token_conditional_swap_count;
out_header.openbook_v2_count = header.openbook_v2_count;
let mut account = MangoAccountRefMut {
header: &mut out_header,
fixed: bytemuck::from_bytes_mut(fixed),
dynamic,
};
account.write_token_conditional_swap_length();
account.write_openbook_v2_length();
MangoAccountValue::from_bytes(&bytes).unwrap()
let mut account = MangoAccountValue::from_bytes(&bytes).unwrap();
// Initialize the openbook orders with defaults as they would be in the program
for i in 0..header.openbook_v2_count() {
*account.openbook_v2_orders_mut_by_raw_index(i) = OpenbookV2Orders::default();
}
account
}
fn check_account_active_and_order(
@ -2428,6 +2714,7 @@ mod tests {
perp_count: 6,
perp_oo_count: 7,
token_conditional_swap_count: 8,
openbook_v2_count: 5,
};
let mut account = make_resize_test_account(&header);
@ -2466,12 +2753,18 @@ mod tests {
make_tcs(2, 0);
make_tcs(4, 1);
account.create_openbook_v2_orders(0)?;
account.create_openbook_v2_orders(7)?;
account.create_openbook_v2_orders(1)?;
*account.openbook_v2_orders_mut_by_raw_index(1) = OpenbookV2Orders::default();
let active = MangoAccountDynamicHeader {
token_count: 2,
serum3_count: 2,
perp_count: 4,
perp_oo_count: 5,
token_conditional_swap_count: 2,
openbook_v2_count: 2,
};
// Resizing to the same size just removes the empty spaces
@ -2483,6 +2776,7 @@ mod tests {
header.perp_count,
header.perp_oo_count,
header.token_conditional_swap_count,
header.openbook_v2_count,
)?;
check_account_active_and_order(&ta, &active)?;
}
@ -2496,6 +2790,7 @@ mod tests {
active.perp_count,
active.perp_oo_count,
active.token_conditional_swap_count,
active.openbook_v2_count,
)?;
check_account_active_and_order(&ta, &active)?;
}
@ -2509,6 +2804,7 @@ mod tests {
active.perp_count,
active.perp_oo_count,
active.token_conditional_swap_count,
active.openbook_v2_count,
)
.unwrap_err();
ta.resize_dynamic_content(
@ -2517,6 +2813,7 @@ mod tests {
active.perp_count,
active.perp_oo_count,
active.token_conditional_swap_count,
active.openbook_v2_count,
)
.unwrap_err();
ta.resize_dynamic_content(
@ -2525,6 +2822,7 @@ mod tests {
active.perp_count - 1,
active.perp_oo_count,
active.token_conditional_swap_count,
active.openbook_v2_count,
)
.unwrap_err();
ta.resize_dynamic_content(
@ -2533,6 +2831,7 @@ mod tests {
active.perp_count,
active.perp_oo_count - 1,
active.token_conditional_swap_count,
active.openbook_v2_count,
)
.unwrap_err();
ta.resize_dynamic_content(
@ -2541,6 +2840,16 @@ mod tests {
active.perp_count,
active.perp_oo_count,
active.token_conditional_swap_count - 1,
active.openbook_v2_count,
)
.unwrap_err();
ta.resize_dynamic_content(
active.token_count,
active.serum3_count,
active.perp_count,
active.perp_oo_count,
active.token_conditional_swap_count,
active.openbook_v2_count - 1,
)
.unwrap_err();
}
@ -2559,6 +2868,7 @@ mod tests {
perp_count: 4,
perp_oo_count: 8,
token_conditional_swap_count: 4,
openbook_v2_count: 2,
};
let mut account = make_resize_test_account(&header);
@ -2569,6 +2879,7 @@ mod tests {
perp_oo_count: rng.gen_range(0..header.perp_oo_count + 1),
token_conditional_swap_count: rng
.gen_range(0..header.token_conditional_swap_count + 1),
openbook_v2_count: rng.gen_range(0..header.openbook_v2_count + 1),
};
let options = (0..header.token_count()).collect_vec();
@ -2603,12 +2914,21 @@ mod tests {
tcs.id = i as u64;
}
let options = (0..header.openbook_v2_count()).collect_vec();
let selected = options.choose_multiple(&mut rng, active.openbook_v2_count());
for (i, index) in selected.sorted().enumerate() {
account
.openbook_v2_orders_mut_by_raw_index(*index)
.market_index = i as OpenbookV2MarketIndex;
}
let target = MangoAccountDynamicHeader {
token_count: rng.gen_range(active.token_count..6),
serum3_count: rng.gen_range(active.serum3_count..7),
serum3_count: rng.gen_range(active.serum3_count..6),
perp_count: rng.gen_range(active.perp_count..6),
perp_oo_count: rng.gen_range(active.perp_oo_count..16),
token_conditional_swap_count: rng.gen_range(active.token_conditional_swap_count..8),
token_conditional_swap_count: rng.gen_range(active.token_conditional_swap_count..6),
openbook_v2_count: rng.gen_range(active.openbook_v2_count..4),
};
let target_size = target.account_size();
@ -2625,6 +2945,7 @@ mod tests {
target.perp_count,
target.perp_oo_count,
target.token_conditional_swap_count,
target.openbook_v2_count,
)
.unwrap();
@ -2881,7 +3202,7 @@ mod tests {
// Grab live accounts with
// solana account CZGf1qbYPaSoabuA1EmdN8W5UHvH5CeXcNZ7RTx65aVQ --output-file programs/mango-v4/resources/test/mangoaccount-v0.21.3.bin
let fixtures = vec!["mangoaccount-v0.21.3"];
let fixtures = vec!["mangoaccount-v0.21.3", "mangoaccount-v0.23.0"];
for fixture in fixtures {
let filename = format!("resources/test/{}.bin", fixture);
@ -2938,6 +3259,12 @@ mod tests {
.cloned()
.collect_vec(),
padding9: Default::default(),
openbook_v2: zerocopy_reader
.all_openbook_v2_orders()
.cloned()
.collect_vec(),
reserved_dynamic: zerocopy_reader.dynamic_reserved_bytes().try_into().unwrap(),
};
@ -2955,3 +3282,17 @@ mod tests {
Ok(())
}
}
#[macro_export]
macro_rules! mango_account_seeds {
( $account:expr ) => {
&[
b"MangoAccount".as_ref(),
$account.group.as_ref(),
$account.owner.as_ref(),
&$account.account_num.to_le_bytes(),
&[$account.bump],
]
};
}
pub use mango_account_seeds;

View File

@ -202,6 +202,101 @@ impl Default for Serum3Orders {
}
}
#[zero_copy]
#[derive(AnchorSerialize, AnchorDeserialize, Derivative, PartialEq)]
#[derivative(Debug)]
pub struct OpenbookV2Orders {
pub open_orders: Pubkey,
/// Tracks the amount of borrows that have flowed into the open orders account.
/// These borrows did not have the loan origination fee applied, and that may happen
/// later (in openbook_v2_settle_funds) if we can guarantee that the funds were used.
/// In particular a place-on-book, cancel, settle should not cost fees.
pub base_borrows_without_fee: u64,
pub quote_borrows_without_fee: u64,
/// Track something like the highest open bid / lowest open ask, in native/native units.
///
/// Tracking it exactly isn't possible since we don't see fills. So instead track
/// the min/max of the _placed_ bids and asks.
///
/// The value is reset in openbook_v2_place_order when a new order is placed without an
/// existing one on the book.
///
/// 0 is a special "unset" state.
pub highest_placed_bid_inv: f64,
pub lowest_placed_ask: f64,
/// An overestimate of the amount of tokens that might flow out of the open orders account.
///
/// The bank still considers these amounts user deposits (see Bank::potential_openbook_tokens)
/// and that value needs to be updated in conjunction with these numbers.
///
/// This estimation is based on the amount of tokens in the open orders account
/// (see update_bank_potential_tokens() in openbook_v2_place_order and settle)
pub potential_base_tokens: u64,
pub potential_quote_tokens: u64,
/// Track lowest bid/highest ask, same way as for highest bid/lowest ask.
///
/// 0 is a special "unset" state.
pub lowest_placed_bid_inv: f64,
pub highest_placed_ask: f64,
/// Stores the market's lot sizes
///
/// Needed because the obv2 open orders account tells us about reserved amounts in lots and
/// we want to be able to compute health without also loading the obv2 market.
pub quote_lot_size: i64,
pub base_lot_size: i64,
pub market_index: OpenbookV2MarketIndex,
/// Store the base/quote token index, so health computations don't need
/// to get passed the static SerumMarket to find which tokens a market
/// uses and look up the correct oracles.
pub base_token_index: TokenIndex,
pub quote_token_index: TokenIndex,
#[derivative(Debug = "ignore")]
pub reserved: [u8; 162],
}
const_assert_eq!(size_of::<OpenbookV2Orders>(), 32 + 8 * 10 + 2 * 3 + 162);
const_assert_eq!(size_of::<OpenbookV2Orders>(), 280);
const_assert_eq!(size_of::<OpenbookV2Orders>() % 8, 0);
impl OpenbookV2Orders {
pub fn is_active(&self) -> bool {
self.market_index != OpenbookV2MarketIndex::MAX
}
pub fn is_active_for_market(&self, market_index: OpenbookV2MarketIndex) -> bool {
self.market_index == market_index
}
}
impl Default for OpenbookV2Orders {
fn default() -> Self {
Self {
open_orders: Pubkey::default(),
market_index: OpenbookV2MarketIndex::MAX,
base_token_index: TokenIndex::MAX,
quote_token_index: TokenIndex::MAX,
base_borrows_without_fee: 0,
quote_borrows_without_fee: 0,
highest_placed_bid_inv: 0.0,
lowest_placed_bid_inv: 0.0,
highest_placed_ask: 0.0,
lowest_placed_ask: 0.0,
potential_base_tokens: 0,
potential_quote_tokens: 0,
quote_lot_size: 0,
base_lot_size: 0,
reserved: [0; 162],
}
}
}
#[zero_copy]
#[derive(AnchorSerialize, AnchorDeserialize, Derivative, PartialEq)]
#[derivative(Debug)]

View File

@ -15,28 +15,30 @@ pub struct OpenbookV2Market {
pub base_token_index: TokenIndex,
// ABI: Clients rely on this being at offset 42
pub quote_token_index: TokenIndex,
pub market_index: OpenbookV2MarketIndex,
pub reduce_only: u8,
pub force_close: u8,
pub padding1: [u8; 2],
pub name: [u8; 16],
pub openbook_v2_program: Pubkey,
pub openbook_v2_market_external: Pubkey,
pub market_index: OpenbookV2MarketIndex,
pub registration_time: u64,
/// Limit orders must be <= oracle * (1+band) and >= oracle / (1+band)
///
/// Zero value is the default due to migration and disables the limit,
/// same as f32::MAX.
pub oracle_price_band: f32,
pub bump: u8,
pub padding2: [u8; 5],
pub registration_time: u64,
pub reserved: [u8; 512],
pub reserved: [u8; 1027],
}
const_assert_eq!(
size_of::<OpenbookV2Market>(),
32 + 2 + 2 + 1 + 3 + 16 + 2 * 32 + 2 + 1 + 5 + 8 + 512
32 + 2 * 3 + 1 * 2 + 1 * 16 + 32 * 2 + 8 + 4 + 1 + 1027
);
const_assert_eq!(size_of::<OpenbookV2Market>(), 648);
const_assert_eq!(size_of::<OpenbookV2Market>(), 1160);
const_assert_eq!(size_of::<OpenbookV2Market>() % 8, 0);
impl OpenbookV2Market {
@ -53,6 +55,14 @@ impl OpenbookV2Market {
pub fn is_force_close(&self) -> bool {
self.force_close == 1
}
pub fn oracle_price_band(&self) -> f32 {
if self.oracle_price_band == 0.0 {
f32::MAX // default disabled
} else {
self.oracle_price_band
}
}
}
#[account(zero_copy)]

View File

@ -32,6 +32,7 @@ mod test_liq_perps_force_cancel;
mod test_liq_perps_positive_pnl;
mod test_liq_tokens;
mod test_margin_trade;
mod test_openbook_v2;
mod test_perp;
mod test_perp_settle;
mod test_perp_settle_fees;

View File

@ -1,5 +1,5 @@
use super::*;
use mango_client::StubOracleCloseInstruction;
// This is an unspecific happy-case test that just runs a few instructions to check
// that they work in principle. It should be split up / renamed.
#[tokio::test]
@ -38,6 +38,7 @@ async fn test_basic() -> Result<(), TransportError> {
perp_count: 3,
perp_oo_count: 3,
token_conditional_swap_count: 3,
openbook_v2_count: 3,
group,
owner,
payer,
@ -339,6 +340,7 @@ async fn test_account_size_migration() -> Result<(), TransportError> {
perp_count: 3,
perp_oo_count: 3,
token_conditional_swap_count: 3,
openbook_v2_count: 3,
group,
owner,
payer,
@ -367,9 +369,9 @@ async fn test_account_size_migration() -> Result<(), TransportError> {
for _ in 0..10 {
new_bytes.extend_from_slice(&bytemuck::bytes_of(&PerpPosition::default()));
}
// remove the 64 reserved bytes at the end
// remove the 56 reserved bytes at the end
new_bytes
.extend_from_slice(&mango_account.dynamic[perp_start..mango_account.dynamic.len() - 64]);
.extend_from_slice(&mango_account.dynamic[perp_start..mango_account.dynamic.len() - 56]);
account_raw.data = new_bytes.clone();
account_raw.lamports = 1_000_000_000; // 1 SOL is enough
@ -976,6 +978,7 @@ async fn test_sequence_check() -> Result<(), TransportError> {
perp_count: 3,
perp_oo_count: 3,
token_conditional_swap_count: 3,
openbook_v2_count: 3,
group,
owner,
payer,

View File

@ -1,5 +1,6 @@
use super::*;
use anchor_lang::prelude::AccountMeta;
use mango_client::StubOracleCreate;
use mango_v4::accounts_ix::{Serum3OrderType, Serum3SelfTradeBehavior, Serum3Side};
async fn deposit_cu_datapoint(

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,5 @@
use super::*;
use mango_client::StubOracleSetInstruction;
#[tokio::test]
async fn test_perp_settle_pnl_basic() -> Result<(), TransportError> {

View File

@ -2,6 +2,7 @@
use super::*;
use anchor_lang::prelude::AccountMeta;
use mango_client::StubOracleCreate;
use mango_v4::accounts_ix::{Serum3OrderType, Serum3SelfTradeBehavior, Serum3Side};
use mango_v4::serum3_cpi::{load_open_orders_bytes, OpenOrdersSlim};
use std::sync::Arc;
@ -562,7 +563,7 @@ async fn test_serum_loan_origination_fees() -> Result<(), TransportError> {
order_placer.settle().await;
let o = order_placer.mango_serum_orders().await;
// parts of the order ended up on the book an may cause loan origination fees later
// parts of the order ended up on the book and may cause loan origination fees later
assert_eq!(
o.base_borrows_without_fee,
(ask_amount - fill_amount) as u64
@ -2019,7 +2020,7 @@ async fn test_serum_skip_bank() -> Result<(), TransportError> {
struct CommonSetup {
group_with_tokens: GroupWithTokens,
serum_market_cookie: SpotMarketCookie,
serum_market_cookie: SerumMarketCookie,
quote_token: crate::program_test::mango_setup::Token,
base_token: crate::program_test::mango_setup::Token,
order_placer: SerumOrderPlacer,

View File

@ -2,6 +2,7 @@ use std::{path::PathBuf, str::FromStr};
use super::*;
use anchor_lang::prelude::AccountMeta;
use mango_client::StubOracleCreate;
use solana_sdk::account::AccountSharedData;
#[tokio::test]

Binary file not shown.

View File

@ -7,7 +7,8 @@ use anchor_spl::token::{Token, TokenAccount};
use fixed::types::I80F48;
use itertools::Itertools;
use mango_v4::accounts_ix::{
HealthCheckKind, InterestRateParams, Serum3OrderType, Serum3SelfTradeBehavior, Serum3Side,
HealthCheckKind, InterestRateParams, OpenbookV2PlaceOrderType, OpenbookV2SelfTradeBehavior,
OpenbookV2Side, Serum3OrderType, Serum3SelfTradeBehavior, Serum3Side,
};
use mango_v4::state::{MangoAccount, MangoAccountValue};
use solana_program::instruction::Instruction;
@ -310,6 +311,7 @@ async fn derive_health_check_remaining_account_metas(
}
let serum_oos = account.active_serum3_orders().map(|&s| s.open_orders);
let openbook_oos = account.active_openbook_v2_orders().map(|&s| s.open_orders);
let to_account_meta = |pubkey| AccountMeta {
pubkey,
@ -328,6 +330,7 @@ async fn derive_health_check_remaining_account_metas(
.chain(perp_markets.map(to_account_meta))
.chain(perp_oracles.into_iter().map(to_account_meta))
.chain(serum_oos.map(to_account_meta))
.chain(openbook_oos.map(to_account_meta))
.collect()
}
@ -2040,6 +2043,7 @@ pub struct AccountCreateInstruction {
pub perp_count: u8,
pub perp_oo_count: u8,
pub token_conditional_swap_count: u8,
pub openbook_v2_count: u8,
pub group: Pubkey,
pub owner: TestKeypair,
pub payer: TestKeypair,
@ -2049,10 +2053,11 @@ impl Default for AccountCreateInstruction {
AccountCreateInstruction {
account_num: 0,
token_count: 8,
serum3_count: 4,
serum3_count: 2,
perp_count: 4,
perp_oo_count: 16,
token_conditional_swap_count: 1,
openbook_v2_count: 2,
group: Default::default(),
owner: Default::default(),
payer: Default::default(),
@ -2062,7 +2067,7 @@ impl Default for AccountCreateInstruction {
#[async_trait::async_trait(?Send)]
impl ClientInstruction for AccountCreateInstruction {
type Accounts = mango_v4::accounts::AccountCreate;
type Instruction = mango_v4::instruction::AccountCreateV2;
type Instruction = mango_v4::instruction::AccountCreateV3;
async fn to_instruction(
&self,
_account_loader: &(impl ClientAccountLoader + 'async_trait),
@ -2075,6 +2080,7 @@ impl ClientInstruction for AccountCreateInstruction {
perp_count: self.perp_count,
perp_oo_count: self.perp_oo_count,
token_conditional_swap_count: self.token_conditional_swap_count,
openbook_v2_count: self.openbook_v2_count,
name: "my_mango_account".to_string(),
};
@ -5146,6 +5152,707 @@ impl ClientInstruction for TokenConditionalSwapStartInstruction {
}
}
pub struct OpenbookV2RegisterMarketInstruction {
pub group: Pubkey,
pub admin: TestKeypair,
pub payer: TestKeypair,
pub openbook_v2_program: Pubkey,
pub openbook_v2_market_external: Pubkey,
pub base_bank: Pubkey,
pub quote_bank: Pubkey,
pub market_index: OpenbookV2MarketIndex,
}
#[async_trait::async_trait(?Send)]
impl ClientInstruction for OpenbookV2RegisterMarketInstruction {
type Accounts = mango_v4::accounts::OpenbookV2RegisterMarket;
type Instruction = mango_v4::instruction::OpenbookV2RegisterMarket;
async fn to_instruction(
&self,
_account_loader: &(impl ClientAccountLoader + 'async_trait),
) -> (Self::Accounts, instruction::Instruction) {
let program_id = mango_v4::id();
let instruction = Self::Instruction {
market_index: self.market_index,
name: "UUU/usdc".to_string(),
oracle_price_band: f32::MAX,
};
let openbook_v2_market = Pubkey::find_program_address(
&[
b"OpenbookV2Market".as_ref(),
self.group.as_ref(),
self.openbook_v2_market_external.as_ref(),
],
&program_id,
)
.0;
let index_reservation = Pubkey::find_program_address(
&[
b"OpenbookV2Index".as_ref(),
self.group.as_ref(),
&self.market_index.to_le_bytes(),
],
&program_id,
)
.0;
let accounts = Self::Accounts {
group: self.group,
admin: self.admin.pubkey(),
openbook_v2_program: self.openbook_v2_program,
openbook_v2_market_external: self.openbook_v2_market_external,
openbook_v2_market,
index_reservation,
base_bank: self.base_bank,
quote_bank: self.quote_bank,
payer: self.payer.pubkey(),
system_program: System::id(),
};
let instruction = make_instruction(program_id, &accounts, &instruction);
(accounts, instruction)
}
fn signers(&self) -> Vec<TestKeypair> {
vec![self.admin, self.payer]
}
}
pub fn openbook_v2_edit_market_instruction_default() -> mango_v4::instruction::OpenbookV2EditMarket
{
mango_v4::instruction::OpenbookV2EditMarket {
reduce_only_opt: None,
force_close_opt: None,
name_opt: None,
oracle_price_band_opt: None,
}
}
pub struct OpenbookV2EditMarketInstruction {
pub group: Pubkey,
pub admin: TestKeypair,
pub market: Pubkey,
pub options: mango_v4::instruction::OpenbookV2EditMarket,
}
#[async_trait::async_trait(?Send)]
impl ClientInstruction for OpenbookV2EditMarketInstruction {
type Accounts = mango_v4::accounts::OpenbookV2EditMarket;
type Instruction = mango_v4::instruction::OpenbookV2EditMarket;
async fn to_instruction(
&self,
_account_loader: &(impl ClientAccountLoader + 'async_trait),
) -> (Self::Accounts, instruction::Instruction) {
let program_id = mango_v4::id();
let accounts = Self::Accounts {
group: self.group,
admin: self.admin.pubkey(),
market: self.market,
};
let instruction = make_instruction(program_id, &accounts, &self.options);
(accounts, instruction)
}
fn signers(&self) -> Vec<TestKeypair> {
vec![self.admin]
}
}
pub struct OpenbookV2DeregisterMarketInstruction {
pub group: Pubkey,
pub admin: TestKeypair,
pub payer: TestKeypair,
pub openbook_v2_market_external: Pubkey,
pub market_index: OpenbookV2MarketIndex,
}
#[async_trait::async_trait(?Send)]
impl ClientInstruction for OpenbookV2DeregisterMarketInstruction {
type Accounts = mango_v4::accounts::OpenbookV2DeregisterMarket;
type Instruction = mango_v4::instruction::OpenbookV2DeregisterMarket;
async fn to_instruction(
&self,
_account_loader: &(impl ClientAccountLoader + 'async_trait),
) -> (Self::Accounts, instruction::Instruction) {
let program_id = mango_v4::id();
let instruction = Self::Instruction {};
let openbook_v2_market = Pubkey::find_program_address(
&[
b"OpenbookV2Market".as_ref(),
self.group.as_ref(),
self.openbook_v2_market_external.as_ref(),
],
&program_id,
)
.0;
let index_reservation = Pubkey::find_program_address(
&[
b"OpenbookV2Index".as_ref(),
self.group.as_ref(),
&self.market_index.to_le_bytes(),
],
&program_id,
)
.0;
let accounts = Self::Accounts {
group: self.group,
admin: self.admin.pubkey(),
openbook_v2_market,
index_reservation,
sol_destination: self.payer.pubkey(),
token_program: Token::id(),
};
let instruction = make_instruction(program_id, &accounts, &instruction);
(accounts, instruction)
}
fn signers(&self) -> Vec<TestKeypair> {
vec![self.admin]
}
}
pub struct OpenbookV2CreateOpenOrdersInstruction {
pub group: Pubkey,
pub payer: TestKeypair,
pub owner: TestKeypair,
pub account: Pubkey,
pub openbook_v2_market: Pubkey,
pub openbook_v2_program: Pubkey,
pub openbook_v2_market_external: Pubkey,
pub next_open_orders_index: u32,
}
#[async_trait::async_trait(?Send)]
impl ClientInstruction for OpenbookV2CreateOpenOrdersInstruction {
type Accounts = mango_v4::accounts::OpenbookV2CreateOpenOrders;
type Instruction = mango_v4::instruction::OpenbookV2CreateOpenOrders;
async fn to_instruction(
&self,
_account_loader: &(impl ClientAccountLoader + 'async_trait),
) -> (Self::Accounts, instruction::Instruction) {
let program_id = mango_v4::id();
let openbook_program_id = openbook_v2::id();
let instruction = Self::Instruction {};
let open_orders_indexer = Pubkey::find_program_address(
&[b"OpenOrdersIndexer".as_ref(), self.account.as_ref()],
&openbook_program_id,
)
.0;
let open_orders_account = Pubkey::find_program_address(
&[
b"OpenOrders".as_ref(),
self.account.as_ref(),
&(self.next_open_orders_index).to_le_bytes(),
],
&openbook_program_id,
)
.0;
let accounts = Self::Accounts {
group: self.group,
account: self.account,
open_orders_indexer,
open_orders_account,
openbook_v2_program: self.openbook_v2_program,
openbook_v2_market_external: self.openbook_v2_market_external,
openbook_v2_market: self.openbook_v2_market,
payer: self.payer.pubkey(),
authority: self.owner.pubkey(),
system_program: System::id(),
rent: Rent::id(),
};
let instruction = make_instruction(program_id, &accounts, &instruction);
(accounts, instruction)
}
fn signers(&self) -> Vec<TestKeypair> {
vec![self.payer, self.owner]
}
}
pub struct OpenbookV2PlaceOrderInstruction {
pub owner: TestKeypair,
pub account: Pubkey,
pub openbook_v2_market: Pubkey,
pub side: OpenbookV2Side,
pub price_lots: i64,
pub max_base_lots: i64,
pub max_quote_lots_including_fees: i64,
pub client_order_id: u64,
pub order_type: OpenbookV2PlaceOrderType,
pub self_trade_behavior: OpenbookV2SelfTradeBehavior,
pub reduce_only: bool,
pub expiry_timestamp: u64,
pub limit: u8,
}
#[async_trait::async_trait(?Send)]
impl ClientInstruction for OpenbookV2PlaceOrderInstruction {
type Accounts = mango_v4::accounts::OpenbookV2PlaceOrder;
type Instruction = mango_v4::instruction::OpenbookV2PlaceOrder;
async fn to_instruction(
&self,
account_loader: &(impl ClientAccountLoader + 'async_trait),
) -> (Self::Accounts, instruction::Instruction) {
let program_id = mango_v4::id();
let openbook_program_id = openbook_v2::id();
let market: OpenbookV2Market = account_loader.load(&self.openbook_v2_market).await.unwrap();
let external_market: openbook_v2::state::Market = account_loader
.load(&market.openbook_v2_market_external)
.await
.unwrap();
let instruction = Self::Instruction {
side: self.side,
price_lots: self.price_lots,
max_base_lots: self.max_base_lots,
max_quote_lots_including_fees: self.max_quote_lots_including_fees,
client_order_id: self.client_order_id,
order_type: self.order_type,
reduce_only: self.reduce_only,
expiry_timestamp: self.expiry_timestamp,
self_trade_behavior: self.self_trade_behavior,
limit: self.limit,
};
let account = account_loader
.load_mango_account(&self.account)
.await
.unwrap();
let quote_info =
get_mint_info_by_token_index(account_loader, &account, market.quote_token_index).await;
let base_info =
get_mint_info_by_token_index(account_loader, &account, market.base_token_index).await;
let (payer_bank, payer_vault, receiver_bank, market_vault) = match self.side {
OpenbookV2Side::Ask => (
base_info.banks[0],
base_info.vaults[0],
quote_info.banks[0],
external_market.market_base_vault,
),
OpenbookV2Side::Bid => (
quote_info.banks[0],
quote_info.vaults[0],
base_info.banks[0],
external_market.market_quote_vault,
),
};
let health_check_metas =
derive_health_check_remaining_account_metas(account_loader, &account, None, true, None)
.await;
let open_orders_account = account
.all_openbook_v2_orders()
.find(|o| o.is_active_for_market(market.market_index))
.unwrap()
.open_orders;
let accounts = Self::Accounts {
group: account.fixed.group,
account: self.account,
authority: self.owner.pubkey(),
open_orders: open_orders_account,
openbook_v2_program: openbook_program_id,
openbook_v2_market_external: market.openbook_v2_market_external,
openbook_v2_market: self.openbook_v2_market,
bids: external_market.bids,
asks: external_market.asks,
event_heap: external_market.event_heap,
payer_bank,
payer_vault,
receiver_bank,
market_vault,
market_vault_signer: external_market.market_authority,
token_program: Token::id(),
};
let mut instruction = make_instruction(program_id, &accounts, &instruction);
instruction.accounts.extend(health_check_metas.into_iter());
(accounts, instruction)
}
fn signers(&self) -> Vec<TestKeypair> {
vec![self.owner]
}
}
pub struct OpenbookV2CancelOrderInstruction {
pub payer: TestKeypair,
pub account: Pubkey,
pub openbook_v2_market: Pubkey,
pub side: OpenbookV2Side,
pub order_id: u128,
}
#[async_trait::async_trait(?Send)]
impl ClientInstruction for OpenbookV2CancelOrderInstruction {
type Accounts = mango_v4::accounts::OpenbookV2CancelOrder;
type Instruction = mango_v4::instruction::OpenbookV2CancelOrder;
async fn to_instruction(
&self,
account_loader: &(impl ClientAccountLoader + 'async_trait),
) -> (Self::Accounts, instruction::Instruction) {
let program_id = mango_v4::id();
let openbook_program_id = openbook_v2::id();
let instruction = Self::Instruction {
side: self.side,
order_id: self.order_id,
};
let account = account_loader
.load_mango_account(&self.account)
.await
.unwrap();
let market: OpenbookV2Market = account_loader.load(&self.openbook_v2_market).await.unwrap();
let external_market: openbook_v2::state::Market = account_loader
.load(&market.openbook_v2_market_external)
.await
.unwrap();
let open_orders_account = account
.all_openbook_v2_orders()
.find(|o| o.is_active_for_market(market.market_index))
.unwrap()
.open_orders;
let accounts = Self::Accounts {
group: account.fixed.group,
account: self.account,
authority: self.payer.pubkey(),
open_orders: open_orders_account,
openbook_v2_program: openbook_program_id,
openbook_v2_market_external: market.openbook_v2_market_external,
openbook_v2_market: self.openbook_v2_market,
bids: external_market.bids,
asks: external_market.asks,
};
let instruction = make_instruction(program_id, &accounts, &instruction);
(accounts, instruction)
}
fn signers(&self) -> Vec<TestKeypair> {
vec![self.payer]
}
}
pub struct OpenbookV2CancelAllOrdersInstruction {
pub payer: TestKeypair,
pub account: Pubkey,
pub openbook_v2_market: Pubkey,
pub side_opt: Option<OpenbookV2Side>,
pub limit: u8,
}
#[async_trait::async_trait(?Send)]
impl ClientInstruction for OpenbookV2CancelAllOrdersInstruction {
type Accounts = mango_v4::accounts::OpenbookV2CancelOrder;
type Instruction = mango_v4::instruction::OpenbookV2CancelAllOrders;
async fn to_instruction(
&self,
account_loader: &(impl ClientAccountLoader + 'async_trait),
) -> (Self::Accounts, instruction::Instruction) {
let program_id = mango_v4::id();
let openbook_program_id = openbook_v2::id();
let instruction = Self::Instruction {
side_opt: self.side_opt,
limit: self.limit,
};
let account = account_loader
.load_mango_account(&self.account)
.await
.unwrap();
let market: OpenbookV2Market = account_loader.load(&self.openbook_v2_market).await.unwrap();
let external_market: openbook_v2::state::Market = account_loader
.load(&market.openbook_v2_market_external)
.await
.unwrap();
let open_orders_account = account
.all_openbook_v2_orders()
.find(|o| o.is_active_for_market(market.market_index))
.unwrap()
.open_orders;
let accounts = Self::Accounts {
group: account.fixed.group,
account: self.account,
authority: self.payer.pubkey(),
open_orders: open_orders_account,
openbook_v2_program: openbook_program_id,
openbook_v2_market_external: market.openbook_v2_market_external,
openbook_v2_market: self.openbook_v2_market,
bids: external_market.bids,
asks: external_market.asks,
};
let instruction = make_instruction(program_id, &accounts, &instruction);
(accounts, instruction)
}
fn signers(&self) -> Vec<TestKeypair> {
vec![self.payer]
}
}
pub struct OpenbookV2SettleFundsInstruction {
pub owner: TestKeypair,
pub account: Pubkey,
pub openbook_v2_market: Pubkey,
pub fees_to_dao: bool,
}
#[async_trait::async_trait(?Send)]
impl ClientInstruction for OpenbookV2SettleFundsInstruction {
type Accounts = mango_v4::accounts::OpenbookV2SettleFunds;
type Instruction = mango_v4::instruction::OpenbookV2SettleFunds;
async fn to_instruction(
&self,
account_loader: &(impl ClientAccountLoader + 'async_trait),
) -> (Self::Accounts, instruction::Instruction) {
let program_id = mango_v4::id();
let openbook_program_id = openbook_v2::id();
let instruction = Self::Instruction {
fees_to_dao: self.fees_to_dao,
};
let account = account_loader
.load_mango_account(&self.account)
.await
.unwrap();
let market: OpenbookV2Market = account_loader.load(&self.openbook_v2_market).await.unwrap();
let external_market: openbook_v2::state::Market = account_loader
.load(&market.openbook_v2_market_external)
.await
.unwrap();
let quote_info =
get_mint_info_by_token_index(account_loader, &account, market.quote_token_index).await;
let base_info =
get_mint_info_by_token_index(account_loader, &account, market.base_token_index).await;
let open_orders_account = account
.all_openbook_v2_orders()
.find(|o| o.is_active_for_market(market.market_index))
.unwrap()
.open_orders;
let accounts = Self::Accounts {
group: account.fixed.group,
account: self.account,
authority: self.owner.pubkey(),
open_orders: open_orders_account,
openbook_v2_program: openbook_program_id,
openbook_v2_market_external: market.openbook_v2_market_external,
openbook_v2_market: self.openbook_v2_market,
market_base_vault: external_market.market_base_vault,
market_quote_vault: external_market.market_quote_vault,
market_vault_signer: external_market.market_authority,
quote_bank: quote_info.first_bank(),
quote_vault: quote_info.first_vault(),
base_bank: base_info.first_bank(),
base_vault: base_info.first_vault(),
quote_oracle: quote_info.oracle,
base_oracle: base_info.oracle,
token_program: Token::id(),
system_program: System::id(),
};
let instruction = make_instruction(program_id, &accounts, &instruction);
(accounts, instruction)
}
fn signers(&self) -> Vec<TestKeypair> {
vec![self.owner]
}
}
pub struct OpenbookV2CloseOpenOrdersInstruction {
pub owner: TestKeypair,
pub account: Pubkey,
pub sol_destination: Pubkey,
pub openbook_v2_market: Pubkey,
}
#[async_trait::async_trait(?Send)]
impl ClientInstruction for OpenbookV2CloseOpenOrdersInstruction {
type Accounts = mango_v4::accounts::OpenbookV2CloseOpenOrders;
type Instruction = mango_v4::instruction::OpenbookV2CloseOpenOrders;
async fn to_instruction(
&self,
account_loader: &(impl ClientAccountLoader + 'async_trait),
) -> (Self::Accounts, instruction::Instruction) {
let program_id = mango_v4::id();
let openbook_program_id = openbook_v2::id();
let instruction = Self::Instruction {};
let account = account_loader
.load_mango_account(&self.account)
.await
.unwrap();
let market: OpenbookV2Market = account_loader.load(&self.openbook_v2_market).await.unwrap();
let quote_info =
get_mint_info_by_token_index(account_loader, &account, market.quote_token_index).await;
let base_info =
get_mint_info_by_token_index(account_loader, &account, market.base_token_index).await;
let open_orders_indexer = Pubkey::find_program_address(
&[b"OpenOrdersIndexer".as_ref(), self.account.as_ref()],
&openbook_program_id,
)
.0;
let open_orders_account = account
.all_openbook_v2_orders()
.find(|o| o.is_active_for_market(market.market_index))
.unwrap()
.open_orders;
let accounts = Self::Accounts {
group: account.fixed.group,
account: self.account,
authority: self.owner.pubkey(),
openbook_v2_program: openbook_program_id,
openbook_v2_market_external: market.openbook_v2_market_external,
openbook_v2_market: self.openbook_v2_market,
quote_bank: quote_info.first_bank(),
base_bank: base_info.first_bank(),
open_orders_indexer,
open_orders_account,
sol_destination: self.sol_destination,
system_program: System::id(),
token_program: Token::id(),
};
println!(
"{:?}",
vec![
account.fixed.group,
self.account,
self.owner.pubkey(),
openbook_program_id,
market.openbook_v2_market_external,
self.openbook_v2_market,
quote_info.first_bank(),
base_info.first_bank(),
open_orders_indexer,
open_orders_account,
self.sol_destination,
System::id(),
Token::id(),
]
);
let instruction = make_instruction(program_id, &accounts, &instruction);
(accounts, instruction)
}
fn signers(&self) -> Vec<TestKeypair> {
vec![self.owner]
}
}
pub struct OpenbookV2LiqForceCancelInstruction {
pub account: Pubkey,
pub payer: TestKeypair,
pub openbook_v2_market: Pubkey,
}
#[async_trait::async_trait(?Send)]
impl ClientInstruction for OpenbookV2LiqForceCancelInstruction {
type Accounts = mango_v4::accounts::OpenbookV2LiqForceCancelOrders;
type Instruction = mango_v4::instruction::OpenbookV2LiqForceCancelOrders;
async fn to_instruction(
&self,
account_loader: &(impl ClientAccountLoader + 'async_trait),
) -> (Self::Accounts, instruction::Instruction) {
let program_id = mango_v4::id();
let openbook_program_id = openbook_v2::id();
let instruction = Self::Instruction { limit: 10 };
let account = account_loader
.load_mango_account(&self.account)
.await
.unwrap();
let market: OpenbookV2Market = account_loader.load(&self.openbook_v2_market).await.unwrap();
let external_market: openbook_v2::state::Market = account_loader
.load(&market.openbook_v2_market_external)
.await
.unwrap();
let quote_info =
get_mint_info_by_token_index(account_loader, &account, market.quote_token_index).await;
let base_info =
get_mint_info_by_token_index(account_loader, &account, market.base_token_index).await;
let open_orders = account
.all_openbook_v2_orders()
.find(|o| o.is_active_for_market(market.market_index))
.unwrap()
.open_orders;
let health_check_metas =
derive_health_check_remaining_account_metas(account_loader, &account, None, true, None)
.await;
let accounts = Self::Accounts {
group: account.fixed.group,
account: self.account,
payer: self.payer.pubkey(),
open_orders,
openbook_v2_program: openbook_program_id,
openbook_v2_market_external: market.openbook_v2_market_external,
openbook_v2_market: self.openbook_v2_market,
bids: external_market.bids,
asks: external_market.asks,
event_heap: external_market.event_heap,
market_base_vault: external_market.market_base_vault,
market_quote_vault: external_market.market_quote_vault,
market_vault_signer: external_market.market_authority,
quote_bank: quote_info.first_bank(),
quote_vault: quote_info.first_vault(),
base_bank: base_info.first_bank(),
base_vault: base_info.first_vault(),
system_program: System::id(),
token_program: Token::id(),
};
let mut instruction = make_instruction(program_id, &accounts, &instruction);
instruction.accounts.extend(health_check_metas.into_iter());
(accounts, instruction)
}
fn signers(&self) -> Vec<TestKeypair> {
vec![self.payer]
}
}
#[derive(Clone)]
pub struct TokenChargeCollateralFeesInstruction {
pub account: Pubkey,

View File

@ -4,7 +4,7 @@ use anchor_lang::prelude::*;
use super::mango_client::*;
use super::solana::SolanaCookie;
use super::{send_tx, MintCookie, TestKeypair, UserCookie};
use super::{MintCookie, TestKeypair, UserCookie};
#[derive(Default)]
pub struct GroupWithTokensConfig {

View File

@ -11,6 +11,7 @@ use spl_token::{state::*, *};
pub use cookies::*;
pub use mango_client::*;
pub use openbook_setup::*;
pub use serum::*;
pub use solana::*;
pub use utils::*;
@ -18,6 +19,8 @@ pub use utils::*;
pub mod cookies;
pub mod mango_client;
pub mod mango_setup;
pub mod openbook_client;
pub mod openbook_setup;
pub mod serum;
pub mod solana;
pub mod utils;
@ -188,6 +191,14 @@ impl TestContextBuilder {
serum_program_id
}
pub fn add_openbook_v2_program(&mut self) -> Pubkey {
let openbook_v2_program_id =
Pubkey::from_str("opnb2LAfJYbRMAHHvqjCwQxanZn7ReEHp1k81EohpZb").unwrap();
self.test
.add_program("openbook_v2", openbook_v2_program_id, None);
openbook_v2_program_id
}
pub fn add_margin_trade_program(&mut self) -> MarginTradeCookie {
let program = Pubkey::from_str("J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix").unwrap();
let token_account = TestKeypair::new();
@ -222,6 +233,7 @@ impl TestContextBuilder {
let mints = self.create_mints();
let users = self.create_users(&mints);
let serum_program_id = self.add_serum_program();
let openbook_v2_program_id = self.add_openbook_v2_program();
let solana = self.start().await;
@ -230,11 +242,17 @@ impl TestContextBuilder {
program_id: serum_program_id,
});
let openbook = Arc::new(OpenbookV2Cookie {
solana: solana.clone(),
program_id: openbook_v2_program_id,
});
TestContext {
solana: solana.clone(),
mints,
users,
serum,
openbook,
}
}
@ -257,6 +275,7 @@ pub struct TestContext {
pub mints: Vec<MintCookie>,
pub users: Vec<UserCookie>,
pub serum: Arc<SerumCookie>,
pub openbook: Arc<OpenbookV2Cookie>,
}
impl TestContext {

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,126 @@
#![allow(dead_code)]
use std::sync::Arc;
use bytemuck::cast_ref;
use itertools::Itertools;
use openbook_client::*;
use openbook_v2::state::{EventHeap, EventType, FillEvent, OpenOrdersAccount, OutEvent};
use solana_sdk::pubkey::Pubkey;
use super::*;
pub struct OpenbookListingKeys {
market_key: TestKeypair,
req_q_key: TestKeypair,
event_q_key: TestKeypair,
bids_key: TestKeypair,
asks_key: TestKeypair,
vault_signer_pk: Pubkey,
vault_signer_nonce: u64,
}
#[derive(Clone, Debug)]
pub struct OpenbookMarketCookie {
pub market: Pubkey,
pub event_heap: Pubkey,
pub bids: Pubkey,
pub asks: Pubkey,
pub quote_vault: Pubkey,
pub base_vault: Pubkey,
pub authority: Pubkey,
pub quote_mint: MintCookie,
pub base_mint: MintCookie,
}
pub struct OpenbookV2Cookie {
pub solana: Arc<solana::SolanaCookie>,
pub program_id: Pubkey,
}
impl OpenbookV2Cookie {
pub async fn list_spot_market(
&self,
quote_mint: &MintCookie,
base_mint: &MintCookie,
payer: TestKeypair,
) -> OpenbookMarketCookie {
let collect_fee_admin = TestKeypair::new();
let market = TestKeypair::new();
let res = openbook_client::send_openbook_tx(
self.solana.as_ref(),
CreateMarketInstruction {
collect_fee_admin: collect_fee_admin.pubkey(),
open_orders_admin: None,
close_market_admin: None,
payer: payer,
market,
quote_lot_size: 10,
base_lot_size: 100,
maker_fee: -200,
taker_fee: 400,
base_mint: base_mint.pubkey,
quote_mint: quote_mint.pubkey,
..CreateMarketInstruction::with_new_book_and_heap(self.solana.as_ref(), None, None)
.await
},
)
.await
.unwrap();
OpenbookMarketCookie {
market: market.pubkey(),
event_heap: res.event_heap,
bids: res.bids,
asks: res.asks,
authority: res.market_authority,
quote_vault: res.market_quote_vault,
base_vault: res.market_base_vault,
quote_mint: *quote_mint,
base_mint: *base_mint,
}
}
pub async fn load_open_orders(&self, address: Pubkey) -> OpenOrdersAccount {
self.solana.get_account::<OpenOrdersAccount>(address).await
}
pub async fn consume_spot_events(&self, spot_market_cookie: &OpenbookMarketCookie, limit: u8) {
let event_heap = self
.solana
.get_account::<EventHeap>(spot_market_cookie.event_heap)
.await;
let to_consume = event_heap
.iter()
.map(|(event, _slot)| event)
.take(limit as usize)
.collect_vec();
let open_orders_accounts = to_consume
.into_iter()
.map(
|event| match EventType::try_from(event.event_type).unwrap() {
EventType::Fill => {
let fill: &FillEvent = cast_ref(event);
fill.maker
}
EventType::Out => {
let out: &OutEvent = cast_ref(event);
out.owner
}
},
)
.collect_vec();
openbook_client::send_openbook_tx(
self.solana.as_ref(),
ConsumeEventsInstruction {
consume_events_admin: None,
market: spot_market_cookie.market,
open_orders_accounts,
},
)
.await
.unwrap();
}
}

View File

@ -19,7 +19,7 @@ pub struct ListingKeys {
}
#[derive(Clone, Debug)]
pub struct SpotMarketCookie {
pub struct SerumMarketCookie {
pub market: Pubkey,
pub req_q: Pubkey,
pub event_q: Pubkey,
@ -95,7 +95,7 @@ impl SerumCookie {
&self,
coin_mint: &MintCookie,
pc_mint: &MintCookie,
) -> SpotMarketCookie {
) -> SerumMarketCookie {
let serum_program_id = self.program_id;
let coin_mint_pk = coin_mint.pubkey;
let pc_mint_pk = pc_mint.pubkey;
@ -167,7 +167,7 @@ impl SerumCookie {
.create_token_account(&fee_account_owner, coin_mint.pubkey)
.await;
SpotMarketCookie {
SerumMarketCookie {
market: market_key.pubkey(),
req_q: req_q_key.pubkey(),
event_q: event_q_key.pubkey(),
@ -185,7 +185,7 @@ impl SerumCookie {
pub async fn consume_spot_events(
&self,
spot_market_cookie: &SpotMarketCookie,
spot_market_cookie: &SerumMarketCookie,
open_orders: &[Pubkey],
) {
let mut sorted_oos = open_orders.to_vec();

View File

@ -1,3 +1,3 @@
[toolchain]
channel = "1.69"
channel = "1.70"
components = ["rustfmt", "clippy"]

View File

@ -5,6 +5,7 @@
"name": "mainnet-beta.clarkeni",
"publicKey": "DLdcpC6AsAJ9xeKMR3WhHrN5sM5o7GVVXQhQ5vwisTtz",
"serum3ProgramId": "9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin",
"openbookV2ProgramId": "DPYRy9sn4SfMzqu5FXVoRiuLnseTr7ZYq2rNSJDLV8uN",
"mangoProgramId": "4MangoMjqJ2firMokCjjGgoK8d4MXcrgL7XJaL3w6fVg",
"banks": [
{
@ -122,6 +123,7 @@
}
],
"serum3Markets": [],
"openbookV2Markets": [],
"perpMarkets": []
}
]

View File

@ -0,0 +1,64 @@
import { AnchorProvider, BN, Wallet } from '@coral-xyz/anchor';
import { Connection, Keypair, PublicKey } from '@solana/web3.js';
import * as dotenv from 'dotenv';
import fs from 'fs';
import { MangoClient } from '../../src/client';
import { MANGO_V4_ID } from '../../src/constants';
dotenv.config();
async function addSpotMarket() {
const options = AnchorProvider.defaultOptions();
const connection = new Connection(
'https://mango.devnet.rpcpool.com',
options,
);
// admin
const admin = Keypair.fromSecretKey(
Buffer.from(
JSON.parse(fs.readFileSync(process.env.ADMIN_KEYPAIR!, 'utf-8')),
),
);
const adminWallet = new Wallet(admin);
const adminProvider = new AnchorProvider(connection, adminWallet, options);
const client = await MangoClient.connect(
adminProvider,
'devnet',
MANGO_V4_ID['devnet'],
);
console.log(`Admin ${admin.publicKey.toBase58()}`);
// fetch group
const groupPk = '7SDejCUPsF3g59GgMsmvxw8dJkkJbT3exoH4RZirwnkM';
const group = await client.getGroup(new PublicKey(groupPk));
console.log(`Found group ${group.publicKey.toBase58()}`);
const baseMint = new PublicKey('So11111111111111111111111111111111111111112');
const quoteMint = new PublicKey(
'8FRFC6MoGGkMFQwngccyu69VnYbzykGeez7ignHVAFSN',
); //devnet usdc
const marketPubkey = new PublicKey(
'85o8dcTxhuV5N3LFkF1pKoCBsXhdekgdQeJ8zGEgnBwP',
);
const signature = await client.openbookV2RegisterMarket(
group,
marketPubkey,
group.getFirstBankByMint(baseMint),
group.getFirstBankByMint(quoteMint),
1,
'SOL/USDC',
0,
);
console.log('Tx Successful:', signature);
process.exit();
}
async function main() {
await addSpotMarket();
}
main();

View File

@ -39,7 +39,7 @@ const DEVNET_ORACLES = new Map([
// TODO: should these constants be baked right into client.ts or even program?
const NET_BORROWS_LIMIT_NATIVE = 1 * Math.pow(10, 7) * Math.pow(10, 6);
const GROUP_NUM = Number(process.env.GROUP_NUM || 0);
const GROUP_NUM = Number(process.env.GROUP_NUM || 420);
async function main() {
let sig;

View File

@ -0,0 +1,103 @@
import { AnchorProvider, BN, Wallet } from '@coral-xyz/anchor';
import { Connection, Keypair, PublicKey } from '@solana/web3.js';
import * as dotenv from 'dotenv';
import fs from 'fs';
import { MangoClient } from '../../src/client';
import { MANGO_V4_ID } from '../../src/constants';
import {
Serum3OrderType,
Serum3SelfTradeBehavior,
Serum3Side,
} from '../../src/accounts/serum3';
import { OpenbookV2Side } from '../../src/accounts/openbookV2';
dotenv.config();
async function addSpotMarket() {
const options = AnchorProvider.defaultOptions();
const connection = new Connection(
'https://mango.devnet.rpcpool.com',
options,
);
// admin
const admin = Keypair.fromSecretKey(
Buffer.from(
JSON.parse(fs.readFileSync(process.env.ADMIN_KEYPAIR!, 'utf-8')),
),
);
const adminWallet = new Wallet(admin);
const adminProvider = new AnchorProvider(connection, adminWallet, options);
const client = await MangoClient.connect(
adminProvider,
'devnet',
MANGO_V4_ID['devnet'],
);
console.log(`Admin ${admin.publicKey.toBase58()}`);
const baseMint = new PublicKey('So11111111111111111111111111111111111111112');
const quoteMint = new PublicKey(
'8FRFC6MoGGkMFQwngccyu69VnYbzykGeez7ignHVAFSN',
); //devnet usdc
// fetch group
const groupPk = '7SDejCUPsF3g59GgMsmvxw8dJkkJbT3exoH4RZirwnkM';
const group = await client.getGroup(new PublicKey(groupPk));
console.log(`Found group ${group.publicKey.toBase58()}`);
const account = await client.getMangoAccountForOwner(
group,
adminWallet.publicKey,
0,
true,
true,
);
if (!account) {
console.error('no mango account 0');
return;
}
console.log(
'accountExpand',
await client.accountExpandV3(
group,
account,
account.tokens.length,
account.serum3.length,
account.perps.length,
account.perpOpenOrders.length,
0,
1,
),
);
console.log([...group.openbookV2ExternalMarketsMap.keys()][0]);
const marketPk = new PublicKey(
[...group.openbookV2ExternalMarketsMap.keys()][0],
);
console.log(
'tokenDeposit',
await client.tokenDeposit(group, account, quoteMint, 1000),
);
console.log(
'placeOrder',
await client.openbookV2PlaceOrder(
group,
account,
marketPk,
OpenbookV2Side.bid,
1,
1,
Serum3SelfTradeBehavior.decrementTake,
Serum3OrderType.limit,
420,
32,
),
);
process.exit();
}
async function main() {
await addSpotMarket();
}
main();

View File

@ -1,23 +1,33 @@
import { Idl } from '@coral-xyz/anchor';
import {
IdlEnumVariant,
IdlField,
IdlType,
IdlTypeDef,
} from '@coral-xyz/anchor/dist/cjs/idl';
import { Idl, IdlError } from '@coral-xyz/anchor';
import { IdlField, IdlType, IdlTypeDef } from '@coral-xyz/anchor/dist/cjs/idl';
import fs from 'fs';
const ignoredIx = ['tokenRegister', 'groupEdit', 'tokenEdit'];
const ignoredIx = [
'tokenRegister',
'groupEdit',
'tokenEdit',
'openbookV2EditMarket',
'openbookV2RegisterMarket',
];
const emptyFieldPrefixes = ['padding', 'reserved'];
const skippedErrors = [
// The account data layout moved from (v1 or v2) to the v3 layout for all accounts
['AccountSize', 'MangoAccount', 440, 512],
];
const skippedErrors = {
'0.25.0': [
['Instruction', 'openbookV2CreateOpenOrders'],
['Instruction', 'openbookV2PlaceOrder'],
['Instruction', 'openbookV2PlaceTakerOrder'],
['Instruction', 'openbookV2CancelAllOrders'],
['Account', 'OpenbookV2Market'],
],
};
function isAllowedError(errorTuple): boolean {
return !skippedErrors.some(
function skipError(newIdl, errorTuple): boolean {
const errors = skippedErrors[newIdl.version];
if (!errors) {
return false;
}
return errors.some(
(a) =>
a.length == errorTuple.length &&
a.every((value, index) => value === errorTuple[index]),
@ -36,6 +46,9 @@ function main(): void {
// Old instructions still exist
for (const oldIx of oldIdl.instructions) {
if (skipError(newIdl, ['Instruction', oldIx.name])) {
continue;
}
const newIx = newIdl.instructions.find((x) => x.name == oldIx.name);
if (!newIx) {
console.log(`Error: instruction '${oldIx.name}' was removed`);
@ -117,6 +130,9 @@ function main(): void {
}
for (const oldAcc of oldIdl.accounts ?? []) {
if (skipError(newIdl, ['Account', oldAcc.name])) {
continue;
}
const newAcc = newIdl.accounts?.find((x) => x.name == oldAcc.name);
// Old accounts still exist
@ -130,7 +146,7 @@ function main(): void {
const newSize = accountSize(newIdl, newAcc);
if (
oldSize != newSize &&
isAllowedError(['AccountSize', oldAcc.name, oldSize, newSize])
!skipError(newIdl, ['AccountSize', oldAcc.name, oldSize, newSize])
) {
console.log(`Error: account '${oldAcc.name}' has changed size`);
hasError = true;
@ -292,31 +308,36 @@ function fieldOffset(fields: IdlField[], field: IdlField, idl: Idl): number {
// The following code is essentially copied from anchor's common.ts
//
export function accountSize(idl: Idl, idlAccount: IdlTypeDef): number {
if (idlAccount.type.kind === 'enum') {
const variantSizes = idlAccount.type.variants.map(
(variant: IdlEnumVariant) => {
if (variant.fields === undefined) {
export function accountSize(idl: Idl, idlAccount: IdlTypeDef) {
switch (idlAccount.type.kind) {
case 'struct': {
return idlAccount.type.fields
.map((f) => typeSize(idl, f.type))
.reduce((acc, size) => acc + size, 0);
}
case 'enum': {
const variantSizes = idlAccount.type.variants.map((variant) => {
if (!variant.fields) {
return 0;
}
return variant.fields
.map((f: IdlField | IdlType) => {
if (!(typeof f === 'object' && 'name' in f)) {
throw new Error('Tuple enum variants not yet implemented.');
return typeSize(idl, f);
}
return typeSize(idl, f.type);
})
.reduce((a: number, b: number) => a + b);
},
);
return Math.max(...variantSizes) + 1;
.reduce((acc, size) => acc + size, 0);
});
return Math.max(...variantSizes) + 1;
}
case 'alias': {
return typeSize(idl, idlAccount.type.value);
}
}
if (idlAccount.type.fields === undefined) {
return 0;
}
return idlAccount.type.fields
.map((f) => typeSize(idl, f.type))
.reduce((a, b) => a + b, 0);
}
function typeSize(idl: Idl, ty: IdlType): number {
@ -370,15 +391,15 @@ function typeSize(idl: Idl, ty: IdlType): number {
if ('defined' in ty) {
const filtered = idl.types?.filter((t) => t.name === ty.defined) ?? [];
if (filtered.length !== 1) {
throw new Error(`Type not found: ${JSON.stringify(ty)}`);
throw new IdlError(`Type not found: ${JSON.stringify(ty)}`);
}
const typeDef = filtered[0];
let typeDef = filtered[0];
return accountSize(idl, typeDef);
}
if ('array' in ty) {
const arrayTy = ty.array[0];
const arraySize = ty.array[1];
let arrayTy = ty.array[0];
let arraySize = ty.array[1];
return typeSize(idl, arrayTy) * arraySize;
}
throw new Error(`Invalid type ${JSON.stringify(ty)}`);

60
ts/client/scripts/obv2.ts Normal file
View File

@ -0,0 +1,60 @@
import { AnchorProvider, BN, Wallet } from '@coral-xyz/anchor';
import { OpenBookV2Client } from '@openbook-dex/openbook-v2';
import { Cluster, Connection, Keypair, PublicKey } from '@solana/web3.js';
import fs from 'fs';
import { sendTransaction } from '../src/utils/rpc';
const CLUSTER: Cluster =
(process.env.CLUSTER_OVERRIDE as Cluster) || 'mainnet-beta';
const CLUSTER_URL =
process.env.CLUSTER_URL_OVERRIDE || process.env.MB_CLUSTER_URL;
const USER_KEYPAIR =
process.env.USER_KEYPAIR_OVERRIDE || process.env.MB_PAYER_KEYPAIR;
async function run() {
const conn = new Connection(CLUSTER_URL!, 'processed');
const kp = Keypair.fromSecretKey(
Buffer.from(
JSON.parse(
process.env.KEYPAIR || fs.readFileSync(USER_KEYPAIR!, 'utf-8'),
),
),
);
const wallet = new Wallet(kp);
const provider = new AnchorProvider(conn, wallet, {});
const client: OpenBookV2Client = new OpenBookV2Client(provider, undefined, {
prioritizationFee: 10_000,
});
const ix = await client.createMarketIx(
wallet.publicKey,
'sol-apr22/usdc',
new PublicKey('So11111111111111111111111111111111111111112'), // sol
new PublicKey('8FRFC6MoGGkMFQwngccyu69VnYbzykGeez7ignHVAFSN'), // usdc
new BN(100),
new BN(100),
new BN(100),
new BN(100),
new BN(100),
null,
null,
null,
null,
provider.wallet.publicKey,
);
const res = await sendTransaction(
client.program.provider as AnchorProvider,
ix[0],
[],
{
prioritizationFee: 1,
additionalSigners: ix[1] as any,
},
);
console.log(res);
}
run();

View File

@ -1,10 +1,16 @@
import { BorshAccountsCoder } from '@coral-xyz/anchor';
import { AnchorProvider, BorshAccountsCoder, Wallet } from '@coral-xyz/anchor';
import { Market, Orderbook } from '@project-serum/serum';
import {
MarketAccount,
BookSideAccount,
OpenBookV2Client,
} from '@openbook-dex/openbook-v2';
import { parsePriceData } from '@pythnetwork/client';
import { TOKEN_PROGRAM_ID, unpackAccount } from '@solana/spl-token';
import {
AccountInfo,
AddressLookupTableAccount,
Keypair,
PublicKey,
} from '@solana/web3.js';
import BN from 'bn.js';
@ -15,6 +21,7 @@ import { Id } from '../ids';
import { I80F48 } from '../numbers/I80F48';
import { PriceImpact, computePriceImpactOnJup } from '../risk';
import {
EmptyWallet,
buildFetch,
deepClone,
toNative,
@ -30,6 +37,8 @@ import {
} from './oracle';
import { BookSide, PerpMarket, PerpMarketIndex } from './perp';
import { MarketIndex, Serum3Market } from './serum3';
import { OpenbookV2MarketIndex, OpenbookV2Market } from './openbookV2';
import NodeWallet from '@coral-xyz/anchor/dist/cjs/nodewallet';
export class Group {
static from(
@ -88,6 +97,9 @@ export class Group {
new Map(), // serum3MarketsMapByExternal
new Map(), // serum3MarketsMapByMarketIndex
new Map(), // serum3MarketExternalsMap
new Map(), // openbookV2MarketsMapByExternal
new Map(), // openbookV2MarketsMapByMarketIndex
new Map(), // openbookV2MarketExternalsMap
new Map(), // perpMarketsMapByOracle
new Map(), // perpMarketsMapByMarketIndex
new Map(), // perpMarketsMapByName
@ -128,6 +140,12 @@ export class Group {
public serum3MarketsMapByExternal: Map<string, Serum3Market>,
public serum3MarketsMapByMarketIndex: Map<MarketIndex, Serum3Market>,
public serum3ExternalMarketsMap: Map<string, Market>,
public openbookV2MarketsMapByExternal: Map<string, OpenbookV2Market>,
public openbookV2MarketsMapByMarketIndex: Map<
MarketIndex,
OpenbookV2Market
>,
public openbookV2ExternalMarketsMap: Map<string, MarketAccount>,
public perpMarketsMapByOracle: Map<string, PerpMarket>,
public perpMarketsMapByMarketIndex: Map<PerpMarketIndex, PerpMarket>,
public perpMarketsMapByName: Map<string, PerpMarket>,
@ -157,6 +175,9 @@ export class Group {
this.reloadSerum3Markets(client, ids).then(() =>
this.reloadSerum3ExternalMarkets(client, ids),
),
this.reloadOpenbookV2Markets(client, ids).then(() =>
this.reloadOpenbookV2ExternalMarkets(client, ids),
),
]);
// console.timeEnd('group.reload');
}
@ -292,6 +313,40 @@ export class Group {
);
}
public async reloadOpenbookV2Markets(
client: MangoClient,
ids?: Id,
): Promise<void> {
let openbookV2Markets: OpenbookV2Market[];
if (ids && ids.getOpenbookV2Markets().length) {
openbookV2Markets = (
await client.program.account.openbookV2Market.fetchMultiple(
ids.getOpenbookV2Markets(),
)
).map((account, index) =>
OpenbookV2Market.from(
ids.getOpenbookV2Markets()[index],
account as any,
),
);
} else {
openbookV2Markets = await client.openbookV2GetMarkets(this);
}
this.openbookV2MarketsMapByExternal = new Map(
openbookV2Markets.map((openbookV2Market) => [
openbookV2Market.openbookMarketExternal.toBase58(),
openbookV2Market,
]),
);
this.openbookV2MarketsMapByMarketIndex = new Map(
openbookV2Markets.map((openbookV2Market) => [
openbookV2Market.marketIndex,
openbookV2Market,
]),
);
}
public async reloadSerum3ExternalMarkets(
client: MangoClient,
ids?: Id,
@ -354,6 +409,59 @@ export class Group {
);
}
public async reloadOpenbookV2ExternalMarkets(
client: MangoClient,
ids?: Id,
): Promise<void> {
const openbookClient = new OpenBookV2Client(
new AnchorProvider(
client.connection,
new EmptyWallet(Keypair.generate()),
{
commitment: client.connection.commitment,
},
),
); // readonly client for deserializing accounts
let markets: MarketAccount[] = [];
const externalMarketIds = ids?.getOpenbookV2ExternalMarkets();
if (ids && externalMarketIds && externalMarketIds.length) {
markets = await Promise.all(
(
await client.program.provider.connection.getMultipleAccountsInfo(
externalMarketIds,
)
).map((account, index) => {
if (!account) {
throw new Error(
`Undefined AI for openbook market ${externalMarketIds[index]}!`,
);
}
return openbookClient.decodeMarket(account?.data) as MarketAccount;
}),
);
} else {
markets = await Promise.all(
Array.from(this.openbookV2MarketsMapByExternal.values()).map(
(openbookV2Market) => {
return openbookClient.program.account.market.fetch(
openbookV2Market.openbookMarketExternal,
);
},
),
);
}
this.openbookV2ExternalMarketsMap = new Map(
Array.from(this.openbookV2MarketsMapByExternal.values()).map(
(openbookV2Market, index) => [
openbookV2Market.openbookMarketExternal.toBase58(),
markets[index],
],
),
);
}
public async reloadPerpMarkets(client: MangoClient, ids?: Id): Promise<void> {
let perpMarkets: PerpMarket[];
if (ids && ids.getPerpMarkets().length) {
@ -628,6 +736,19 @@ export class Group {
return serum3Market;
}
public getOpenbookV2MarketByMarketIndex(
marketIndex: MarketIndex,
): OpenbookV2Market {
const openbookV2Market =
this.openbookV2MarketsMapByMarketIndex.get(marketIndex);
if (!openbookV2Market) {
throw new Error(
`No openbookV2Market found for marketIndex ${marketIndex}!`,
);
}
return openbookV2Market;
}
public getSerum3MarketByName(name: string): Serum3Market {
const serum3Market = Array.from(
this.serum3MarketsMapByExternal.values(),
@ -638,6 +759,16 @@ export class Group {
return serum3Market;
}
public getOpenbookV2MarketByName(name: string): OpenbookV2Market {
const openbookV2Market = Array.from(
this.openbookV2MarketsMapByExternal.values(),
).find((openbookV2Market) => openbookV2Market.name === name);
if (!openbookV2Market) {
throw new Error(`No openbookV2Market found by name ${name}!`);
}
return openbookV2Market;
}
public getSerum3MarketByExternalMarket(
externalMarketPk: PublicKey,
): Serum3Market {
@ -654,6 +785,22 @@ export class Group {
return serum3Market;
}
public getOpenbookV2MarketByExternalMarket(
externalMarketPk: PublicKey,
): OpenbookV2Market {
const openbookV2Market = Array.from(
this.openbookV2MarketsMapByExternal.values(),
).find((openbookV2Market) =>
openbookV2Market.openbookMarketExternal.equals(externalMarketPk),
);
if (!openbookV2Market) {
throw new Error(
`No openbookV2Market found for external openbookV2 market ${externalMarketPk.toString()}!`,
);
}
return openbookV2Market;
}
public getSerum3ExternalMarket(externalMarketPk: PublicKey): Market {
const market = this.serum3ExternalMarketsMap.get(
externalMarketPk.toBase58(),
@ -666,6 +813,20 @@ export class Group {
return market;
}
public getOpenbookV2ExternalMarket(
externalMarketPk: PublicKey,
): MarketAccount {
const market = this.openbookV2ExternalMarketsMap.get(
externalMarketPk.toBase58(),
);
if (!market) {
throw new Error(
`No openbookV2 external market found for pk ${externalMarketPk.toString()}!`,
);
}
return market;
}
public async loadSerum3BidsForMarket(
client: MangoClient,
externalMarketPk: PublicKey,
@ -682,6 +843,24 @@ export class Group {
return await serum3Market.loadAsks(client, this);
}
public async loadOpenbookV2BidsForMarket(
client: MangoClient,
externalMarketPk: PublicKey,
): Promise<BookSideAccount> {
const openbookV2Market =
this.getOpenbookV2MarketByExternalMarket(externalMarketPk);
return await openbookV2Market.loadBids(client, this);
}
public async loadOpenbookV2AsksForMarket(
client: MangoClient,
externalMarketPk: PublicKey,
): Promise<BookSideAccount> {
const openbookV2Market =
this.getOpenbookV2MarketByExternalMarket(externalMarketPk);
return await openbookV2Market.loadAsks(client, this);
}
public findPerpMarket(marketIndex: PerpMarketIndex): PerpMarket {
const perpMarket = Array.from(this.perpMarketsMapByName.values()).find(
(perpMarket) => perpMarket.perpMarketIndex === marketIndex,

View File

@ -38,6 +38,8 @@ describe('Mango Account', () => {
[],
[],
[],
[],
new Map(),
new Map(),
);
@ -112,6 +114,8 @@ describe('maxWithdraw', () => {
[],
[],
[],
[],
new Map(),
new Map(),
);
protoAccount.tokens.push(

View File

@ -1,7 +1,8 @@
import { AnchorProvider, BN } from '@coral-xyz/anchor';
import { AnchorProvider, BN, Wallet } from '@coral-xyz/anchor';
import { utf8 } from '@coral-xyz/anchor/dist/cjs/utils/bytes';
import { OpenOrders, Order, Orderbook } from '@project-serum/serum/lib/market';
import { AccountInfo, PublicKey } from '@solana/web3.js';
import { OpenOrdersAccount, OpenBookV2Client } from '@openbook-dex/openbook-v2';
import { AccountInfo, Keypair, PublicKey } from '@solana/web3.js';
import { MangoClient } from '../client';
import { OPENBOOK_PROGRAM_ID, RUST_I64_MAX, RUST_I64_MIN } from '../constants';
import {
@ -12,6 +13,7 @@ import {
ZERO_I80F48,
} from '../numbers/I80F48';
import {
EmptyWallet,
U64_MAX_BN,
deepClone,
roundTo5,
@ -30,6 +32,7 @@ export class MangoAccount {
public name: string;
public tokens: TokenPosition[];
public serum3: Serum3Orders[];
public openbookV2: OpenbookV2Orders[];
public perps: PerpPosition[];
public perpOpenOrders: PerpOo[];
public tokenConditionalSwaps: TokenConditionalSwap[];
@ -55,6 +58,7 @@ export class MangoAccount {
headerVersion: number;
tokens: unknown;
serum3: unknown;
openbookV2: unknown;
perps: unknown;
perpOpenOrders: unknown;
tokenConditionalSwaps: unknown;
@ -80,10 +84,12 @@ export class MangoAccount {
obj.headerVersion,
obj.tokens as TokenPositionDto[],
obj.serum3 as Serum3PositionDto[],
obj.openbookV2 as OpenbookV2PositionDto[],
obj.perps as PerpPositionDto[],
obj.perpOpenOrders as PerpOoDto[],
obj.tokenConditionalSwaps as TokenConditionalSwapDto[],
new Map(), // serum3OosMapByMarketIndex
new Map(), // openbookV2OosMapByMarketIndex
);
}
@ -107,14 +113,17 @@ export class MangoAccount {
public headerVersion: number,
tokens: TokenPositionDto[],
serum3: Serum3PositionDto[],
openbookV2: OpenbookV2PositionDto[],
perps: PerpPositionDto[],
perpOpenOrders: PerpOoDto[],
tokenConditionalSwaps: TokenConditionalSwapDto[],
public serum3OosMapByMarketIndex: Map<number, OpenOrders>,
public openbookV2OosMapByMarketIndex: Map<number, OpenOrdersAccount>,
) {
this.name = utf8.decode(new Uint8Array(name)).split('\x00')[0];
this.tokens = tokens.map((dto) => TokenPosition.from(dto));
this.serum3 = serum3.map((dto) => Serum3Orders.from(dto));
this.openbookV2 = openbookV2.map((dto) => OpenbookV2Orders.from(dto));
this.perps = perps.map((dto) => PerpPosition.from(dto));
this.perpOpenOrders = perpOpenOrders.map((dto) => PerpOo.from(dto));
this.tokenConditionalSwaps = tokenConditionalSwaps.map((dto) =>
@ -125,6 +134,7 @@ export class MangoAccount {
public async reload(client: MangoClient): Promise<MangoAccount> {
const mangoAccount = await client.getMangoAccount(this.publicKey);
await mangoAccount.reloadSerum3OpenOrders(client);
await mangoAccount.reloadOpenbookV2OpenOrders(client);
Object.assign(this, mangoAccount);
return mangoAccount;
}
@ -134,6 +144,7 @@ export class MangoAccount {
): Promise<{ value: MangoAccount; slot: number }> {
const resp = await client.getMangoAccountWithSlot(this.publicKey);
await resp?.value.reloadSerum3OpenOrders(client);
await resp?.value.reloadOpenbookV2OpenOrders(client);
Object.assign(this, resp?.value);
return { value: resp!.value, slot: resp!.slot };
}
@ -166,6 +177,43 @@ export class MangoAccount {
return this;
}
async reloadOpenbookV2OpenOrders(client: MangoClient): Promise<MangoAccount> {
const openbookClient = new OpenBookV2Client(
new AnchorProvider(
client.connection,
new EmptyWallet(Keypair.generate()),
{
commitment: client.connection.commitment,
},
),
); // readonly client for deserializing accounts
const openbookV2Active = this.openbookV2Active();
if (!openbookV2Active.length) return this;
const ais =
await client.program.provider.connection.getMultipleAccountsInfo(
openbookV2Active.map((openbookV2) => openbookV2.openOrders),
);
this.openbookV2OosMapByMarketIndex = new Map(
Array.from(
ais.map((ai, i) => {
if (!ai) {
throw new Error(
`Undefined AI for open orders ${openbookV2Active[i].openOrders} and market ${openbookV2Active[i].marketIndex}!`,
);
}
const oo =
openbookClient.program.account.openOrdersAccount.coder.accounts.decode(
'openOrdersAccount',
ai.data,
);
return [openbookV2Active[i].marketIndex, oo];
}),
),
);
return this;
}
loadSerum3OpenOrders(serum3OosMapByOo: Map<string, OpenOrders>): void {
const serum3Active = this.serum3Active();
if (!serum3Active.length) return;
@ -182,6 +230,24 @@ export class MangoAccount {
);
}
loadOpenbookV2OpenOrders(
openbookV2OosMapByOo: Map<string, OpenOrdersAccount>,
): void {
const openbookV2Active = this.openbookV2Active();
if (!openbookV2Active.length) return;
this.openbookV2OosMapByMarketIndex = new Map(
Array.from(
openbookV2Active.map((mangoOo) => {
const oo = openbookV2OosMapByOo.get(mangoOo.openOrders.toBase58());
if (!oo) {
throw new Error(`Undefined open orders for ${mangoOo.openOrders}`);
}
return [mangoOo.marketIndex, oo];
}),
),
);
}
public isDelegate(client: MangoClient): boolean {
return this.delegate.equals(
(client.program.provider as AnchorProvider).wallet.publicKey,
@ -211,6 +277,10 @@ export class MangoAccount {
return this.serum3.filter((serum3) => serum3.isActive());
}
public openbookV2Active(): OpenbookV2Orders[] {
return this.openbookV2.filter((openbookV2) => openbookV2.isActive());
}
public tokenConditionalSwapsActive(): TokenConditionalSwap[] {
return this.tokenConditionalSwaps.filter((tcs) => tcs.isConfigured);
}
@ -245,6 +315,12 @@ export class MangoAccount {
return this.serum3.find((sa) => sa.marketIndex == marketIndex);
}
public getOpenbookV2Account(
marketIndex: MarketIndex,
): OpenbookV2Orders | undefined {
return this.openbookV2.find((sa) => sa.marketIndex == marketIndex);
}
public getPerpPosition(
perpMarketIndex: PerpMarketIndex,
): PerpPosition | undefined {
@ -270,7 +346,19 @@ export class MangoAccount {
if (!oo) {
throw new Error(
`Open orders account not loaded for market with marketIndex ${marketIndex}!`,
`Serum3 open orders account not loaded for market with marketIndex ${marketIndex}!`,
);
}
return oo;
}
public getOpenbookV2OoAccount(marketIndex: MarketIndex): OpenOrdersAccount {
const oo: OpenOrdersAccount | undefined =
this.openbookV2OosMapByMarketIndex.get(marketIndex);
if (!oo) {
throw new Error(
`Openbook V2 open orders account not loaded for market with marketIndex ${marketIndex}!`,
);
}
return oo;
@ -308,6 +396,20 @@ export class MangoAccount {
bal.add(I80F48.fromI64(oo.quoteTokenFree));
}
}
for (const openbookV2Market of Array.from(
group.openbookV2MarketsMapByMarketIndex.values(),
)) {
const oo = this.openbookV2OosMapByMarketIndex.get(
openbookV2Market.marketIndex,
);
if (openbookV2Market.baseTokenIndex == bank.tokenIndex && oo) {
bal.add(I80F48.fromI64(oo.position.baseFreeNative));
}
if (openbookV2Market.quoteTokenIndex == bank.tokenIndex && oo) {
bal.add(I80F48.fromI64(oo.position.quoteFreeNative));
}
}
return bal;
}
return ZERO_I80F48();
@ -1413,6 +1515,33 @@ export class Serum3Orders {
}
}
export class OpenbookV2Orders {
static OpenbookV2MarketIndexUnset = 65535;
static from(dto: OpenbookV2PositionDto): Serum3Orders {
return new OpenbookV2Orders(
dto.openOrders,
dto.marketIndex as MarketIndex,
dto.baseTokenIndex as TokenIndex,
dto.quoteTokenIndex as TokenIndex,
dto.highestPlacedBidInv,
dto.lowestPlacedAsk,
);
}
constructor(
public openOrders: PublicKey,
public marketIndex: MarketIndex,
public baseTokenIndex: TokenIndex,
public quoteTokenIndex: TokenIndex,
public highestPlacedBidInv: number,
public lowestPlacedAsk: number,
) {}
public isActive(): boolean {
return this.marketIndex !== OpenbookV2Orders.OpenbookV2MarketIndexUnset;
}
}
export class Serum3PositionDto {
constructor(
public openOrders: PublicKey,
@ -1429,6 +1558,20 @@ export class Serum3PositionDto {
) {}
}
export class OpenbookV2PositionDto {
constructor(
public openOrders: PublicKey,
public marketIndex: number,
public baseBorrowsWithoutFee: BN,
public quoteBorrowsWithoutFee: BN,
public baseTokenIndex: number,
public quoteTokenIndex: number,
public highestPlacedBidInv: number,
public lowestPlacedAsk: number,
public reserved: number[],
) {}
}
export interface CumulativeFunding {
cumulativeLongFunding: number;
cumulativeShortFunding: number;

View File

@ -0,0 +1,368 @@
import { utf8 } from '@coral-xyz/anchor/dist/cjs/utils/bytes';
import {
OpenBookV2Client,
BookSideAccount,
MarketAccount,
baseLotsToUi,
priceLotsToUi,
} from '@openbook-dex/openbook-v2';
import { Cluster, Keypair, PublicKey } from '@solana/web3.js';
import BN from 'bn.js';
import { MangoClient } from '../client';
import { OPENBOOK_V2_PROGRAM_ID } from '../constants';
import { MAX_I80F48, ONE_I80F48, ZERO_I80F48 } from '../numbers/I80F48';
import { As, EmptyWallet } from '../utils';
import { TokenIndex } from './bank';
import { Group } from './group';
import { AnchorProvider, Wallet } from '@coral-xyz/anchor';
export type OpenbookV2MarketIndex = number & As<'market-index'>;
export class OpenbookV2Market {
public name: string;
static from(
publicKey: PublicKey,
obj: {
group: PublicKey;
baseTokenIndex: number;
quoteTokenIndex: number;
name: number[];
openbookV2Program: PublicKey;
openbookV2MarketExternal: PublicKey;
marketIndex: number;
registrationTime: BN;
reduceOnly: number;
forceClose: number;
},
): OpenbookV2Market {
return new OpenbookV2Market(
publicKey,
obj.group,
obj.baseTokenIndex as TokenIndex,
obj.quoteTokenIndex as TokenIndex,
obj.name,
obj.openbookV2Program,
obj.openbookV2MarketExternal,
obj.marketIndex as OpenbookV2MarketIndex,
obj.registrationTime,
obj.reduceOnly == 1,
obj.forceClose == 1,
);
}
constructor(
public publicKey: PublicKey,
public group: PublicKey,
public baseTokenIndex: TokenIndex,
public quoteTokenIndex: TokenIndex,
name: number[],
public openbookProgram: PublicKey,
public openbookMarketExternal: PublicKey,
public marketIndex: OpenbookV2MarketIndex,
public registrationTime: BN,
public reduceOnly: boolean,
public forceClose: boolean,
) {
this.name = utf8.decode(new Uint8Array(name)).split('\x00')[0];
}
public findOoIndexerPda(
programId: PublicKey,
mangoAccount: PublicKey,
): PublicKey {
const [openOrderPublicKey] = PublicKey.findProgramAddressSync(
[Buffer.from('OpenOrdersIndexer'), mangoAccount.toBuffer()],
programId,
);
return openOrderPublicKey;
}
public findOoPda(
programId: PublicKey,
mangoAccount: PublicKey,
index: number,
): PublicKey {
const indexBuf = Buffer.alloc(4);
indexBuf.writeUInt32LE(index);
const [openOrderPublicKey] = PublicKey.findProgramAddressSync(
[Buffer.from('OpenOrders'), mangoAccount.toBuffer(), indexBuf],
programId,
);
return openOrderPublicKey;
}
public async getNextOoPda(
client: MangoClient,
programId: PublicKey,
mangoAccount: PublicKey,
): Promise<PublicKey> {
const openbookClient = new OpenBookV2Client(
new AnchorProvider(
client.connection,
new EmptyWallet(Keypair.generate()),
{
commitment: client.connection.commitment,
},
),
);
const indexer =
await openbookClient.program.account.openOrdersIndexer.fetchNullable(
this.findOoIndexerPda(programId, mangoAccount),
);
const nextIndex = indexer ? indexer.createdCounter + 1 : 1;
const indexBuf = Buffer.alloc(4);
indexBuf.writeUInt32LE(nextIndex);
const [openOrderPublicKey] = PublicKey.findProgramAddressSync(
[Buffer.from('OpenOrders'), mangoAccount.toBuffer(), indexBuf],
programId,
);
console.log('nextoo', nextIndex, openOrderPublicKey.toBase58());
return openOrderPublicKey;
}
public getFeeRates(taker = true): number {
// todo-pan: fees are no longer hardcoded!!
// See https://github.com/openbook-dex/program/blob/master/dex/src/fees.rs#L81
const ratesBps =
this.name === 'USDT/USDC'
? { maker: -0.5, taker: 1 }
: { maker: -2, taker: 4 };
return taker ? ratesBps.taker * 0.0001 : ratesBps.maker * 0.0001;
}
/**
*
* @param group
* @returns maximum leverage one can bid on this market, this is only for display purposes,
* also see getMaxQuoteForOpenbookV2BidUi and getMaxBaseForOpenbookV2AskUi
*/
maxBidLeverage(group: Group): number {
const baseBank = group.getFirstBankByTokenIndex(this.baseTokenIndex);
const quoteBank = group.getFirstBankByTokenIndex(this.quoteTokenIndex);
if (
quoteBank.initLiabWeight.sub(baseBank.initAssetWeight).lte(ZERO_I80F48())
) {
return MAX_I80F48().toNumber();
}
return ONE_I80F48()
.div(quoteBank.initLiabWeight.sub(baseBank.initAssetWeight))
.toNumber();
}
/**
*
* @param group
* @returns maximum leverage one can ask on this market, this is only for display purposes,
* also see getMaxQuoteForOpenbookV2BidUi and getMaxBaseForOpenbookV2AskUi
*/
maxAskLeverage(group: Group): number {
const baseBank = group.getFirstBankByTokenIndex(this.baseTokenIndex);
const quoteBank = group.getFirstBankByTokenIndex(this.quoteTokenIndex);
if (
baseBank.initLiabWeight.sub(quoteBank.initAssetWeight).lte(ZERO_I80F48())
) {
return MAX_I80F48().toNumber();
}
return ONE_I80F48()
.div(baseBank.initLiabWeight.sub(quoteBank.initAssetWeight))
.toNumber();
}
public async loadBids(
client: MangoClient,
group: Group,
): Promise<BookSideAccount> {
const openbookClient = new OpenBookV2Client(
new AnchorProvider(
client.connection,
new EmptyWallet(Keypair.generate()),
{
commitment: client.connection.commitment,
},
),
); // readonly client for deserializing accounts
const openbookMarketExternal = group.getOpenbookV2ExternalMarket(
this.openbookMarketExternal,
);
return await openbookClient.program.account.bookSide.fetch(
openbookMarketExternal.bids,
);
}
public async loadAsks(
client: MangoClient,
group: Group,
): Promise<BookSideAccount> {
const openbookClient = new OpenBookV2Client(
new AnchorProvider(
client.connection,
new EmptyWallet(Keypair.generate()),
{
commitment: client.connection.commitment,
},
),
); // readonly client for deserializing accounts
const openbookMarketExternal = group.getOpenbookV2ExternalMarket(
this.openbookMarketExternal,
);
return await openbookClient.program.account.bookSide.fetch(
openbookMarketExternal.asks,
);
}
public async computePriceForMarketOrderOfSize(
client: MangoClient,
group: Group,
size: number,
side: 'buy' | 'sell',
): Promise<number> {
const ob =
side == 'buy'
? await this.loadBids(client, group)
: await this.loadAsks(client, group);
let acc = 0;
let selectedOrder;
const orderSize = size;
const openbookMarketExternal = group.getOpenbookV2ExternalMarket(
this.openbookMarketExternal,
);
for (const order of this.getL2(client, openbookMarketExternal, ob)) {
acc += order[1];
if (acc >= orderSize) {
selectedOrder = order;
break;
}
}
if (!selectedOrder) {
throw new Error(
'Unable to place market order for this order size. Please retry.',
);
}
if (side === 'buy') {
return selectedOrder[0] * 1.05 /* TODO Fix random constant */;
} else {
return selectedOrder[0] * 0.95 /* TODO Fix random constant */;
}
}
public getL2(
client: MangoClient,
marketAccount: MarketAccount,
bidsAccount?: BookSideAccount,
asksAccount?: BookSideAccount,
): [number, number][] {
const openbookClient = new OpenBookV2Client(
new AnchorProvider(
client.connection,
new EmptyWallet(Keypair.generate()),
{
commitment: client.connection.commitment,
},
),
); // readonly client for deserializing accounts
const bidNodes = bidsAccount
? openbookClient.getLeafNodes(bidsAccount)
: [];
const askNodes = asksAccount
? openbookClient.getLeafNodes(asksAccount)
: [];
const levels: [number, number][] = [];
for (const node of bidNodes.concat(askNodes)) {
const priceLots = node.key.shrn(64);
levels.push([
priceLotsToUi(marketAccount, priceLots),
baseLotsToUi(marketAccount, node.quantity),
]);
}
return levels;
}
public async logOb(client: MangoClient, group: Group): Promise<string> {
// todo-pan
const res = ``;
// res += ` ${this.name} OrderBook`;
// let orders = await this?.loadAsks(client, group);
// for (const order of orders!.items(true)) {
// res += `\n ${order.price.toString().padStart(10)}, ${order.size
// .toString()
// .padStart(10)}`;
// }
// res += `\n --------------------------`;
// orders = await this?.loadBids(client, group);
// for (const order of orders!.items(true)) {
// res += `\n ${order.price.toString().padStart(10)}, ${order.size
// .toString()
// .padStart(10)}`;
// }
return res;
}
}
export type OpenbookV2OrderType =
| { limit: Record<string, never> }
| { immediateOrCancel: Record<string, never> }
| { postOnly: Record<string, never> };
// eslint-disable-next-line @typescript-eslint/no-namespace
export namespace OpenbookV2OrderType {
export const limit = { limit: {} };
export const immediateOrCancel = { immediateOrCancel: {} };
export const postOnly = { postOnly: {} };
}
export type OpenbookV2SelfTradeBehavior =
| { decrementTake: Record<string, never> }
| { cancelProvide: Record<string, never> }
| { abortTransaction: Record<string, never> };
// eslint-disable-next-line @typescript-eslint/no-namespace
export namespace OpenbookV2SelfTradeBehavior {
export const decrementTake = { decrementTake: {} };
export const cancelProvide = { cancelProvide: {} };
export const abortTransaction = { abortTransaction: {} };
}
export type OpenbookV2Side =
| { bid: Record<string, never> }
| { ask: Record<string, never> };
// eslint-disable-next-line @typescript-eslint/no-namespace
export namespace OpenbookV2Side {
export const bid = { bid: {} };
export const ask = { ask: {} };
}
export function generateOpenbookV2MarketExternalVaultSignerAddress(
openbookV2Market: OpenbookV2Market,
): PublicKey {
return PublicKey.findProgramAddressSync(
[Buffer.from('Market'), openbookV2Market.openbookMarketExternal.toBuffer()],
openbookV2Market.openbookProgram,
)[0];
}
export function priceNumberToLots(price: number, market: MarketAccount): BN {
return new BN(
Math.round(
(price *
Math.pow(10, market.quoteDecimals) *
market.baseLotSize.toNumber()) /
(Math.pow(10, market.baseDecimals) * market.quoteLotSize.toNumber()),
),
);
}
export function baseSizeNumberToLots(size: number, market: MarketAccount): BN {
const native = new BN(Math.round(size * Math.pow(10, market.baseDecimals)));
// rounds down to the nearest lot size
return native.div(market.baseLotSize);
}

File diff suppressed because it is too large Load Diff

View File

@ -36,7 +36,7 @@ export interface TokenRegisterParams {
export const DefaultTokenRegisterParams: TokenRegisterParams = {
oracleConfig: {
confFilter: 0,
confFilter: 0.3,
maxStalenessSlots: null,
},
groupInsuranceFund: false,
@ -312,6 +312,8 @@ export interface IxGateParams {
TokenForceWithdraw: boolean;
SequenceCheck: boolean;
HealthCheck: boolean;
OpenbookV2CancelAllOrders: boolean;
GroupChangeInsuranceFund: boolean;
}
// Default with all ixs enabled, use with buildIxGate
@ -394,6 +396,8 @@ export const TrueIxGateParams: IxGateParams = {
TokenForceWithdraw: true,
SequenceCheck: true,
HealthCheck: true,
OpenbookV2CancelAllOrders: true,
GroupChangeInsuranceFund: true,
};
// build ix gate e.g. buildIxGate(Builder(TrueIxGateParams).TokenDeposit(false).build()).toNumber(),
@ -486,7 +490,8 @@ export function buildIxGate(p: IxGateParams): BN {
toggleIx(ixGate, p, 'TokenForceWithdraw', 72);
toggleIx(ixGate, p, 'SequenceCheck', 73);
toggleIx(ixGate, p, 'HealthCheck', 74);
toggleIx(ixGate, p, 'GroupChangeInsuranceFund', 75);
toggleIx(ixGate, p, 'OpenbookV2CancelAllOrders', 75);
toggleIx(ixGate, p, 'GroupChangeInsuranceFund', 76);
return ixGate;
}

View File

@ -20,6 +20,12 @@ export const OPENBOOK_PROGRAM_ID = {
'mainnet-beta': new PublicKey('srmqPvymJeFKQ4zGQed1GFppgkRHL9kaELCbyksJtPX'),
};
export const OPENBOOK_V2_PROGRAM_ID = {
testnet: new PublicKey('opnb2LAfJYbRMAHHvqjCwQxanZn7ReEHp1k81EohpZb'),
devnet: new PublicKey('opnb2LAfJYbRMAHHvqjCwQxanZn7ReEHp1k81EohpZb'),
'mainnet-beta': new PublicKey('opnb2LAfJYbRMAHHvqjCwQxanZn7ReEHp1k81EohpZb'),
};
export const MANGO_V4_ID = {
testnet: new PublicKey('4MangoMjqJ2firMokCjjGgoK8d4MXcrgL7XJaL3w6fVg'),
devnet: new PublicKey('4MangoMjqJ2firMokCjjGgoK8d4MXcrgL7XJaL3w6fVg'),

View File

@ -7,6 +7,7 @@ export class Id {
public name: string,
public publicKey: string,
public serum3ProgramId: string,
public openbookV2ProgramId: string,
public mangoProgramId: string,
public banks: {
name: string;
@ -24,6 +25,12 @@ export class Id {
active: boolean;
marketExternal: string;
}[],
public openbookV2Markets: {
name: string;
publicKey: string;
active: boolean;
marketExternal: string;
}[],
public perpMarkets: { name: string; publicKey: string; active: boolean }[],
) {}
@ -63,6 +70,24 @@ export class Id {
);
}
public getOpenbookV2Markets(): PublicKey[] {
return Array.from(
this.openbookV2Markets
.filter((openbookV2Market) => openbookV2Market.active)
.map((openbookV2Market) => new PublicKey(openbookV2Market.publicKey)),
);
}
public getOpenbookV2ExternalMarkets(): PublicKey[] {
return Array.from(
this.openbookV2Markets
.filter((openbookV2Market) => openbookV2Market.active)
.map(
(openbookV2Market) => new PublicKey(openbookV2Market.marketExternal),
),
);
}
public getPerpMarkets(): PublicKey[] {
return Array.from(
this.perpMarkets.map((perpMarket) => new PublicKey(perpMarket.publicKey)),
@ -78,11 +103,13 @@ export class Id {
groupConfig.name,
groupConfig.publicKey,
groupConfig.serum3ProgramId,
groupConfig.openbookV2ProgramId,
groupConfig.mangoProgramId,
groupConfig['banks'],
groupConfig['stubOracles'],
groupConfig['mintInfos'],
groupConfig['serum3Markets'],
groupConfig['openbookV2Markets'],
groupConfig['perpMarkets'],
);
}
@ -99,11 +126,13 @@ export class Id {
groupConfig.name,
groupConfig.publicKey,
groupConfig.serum3ProgramId,
groupConfig.openbookV2ProgramId,
groupConfig.mangoProgramId,
groupConfig['banks'],
groupConfig['stubOracles'],
groupConfig['mintInfos'],
groupConfig['serum3Markets'],
groupConfig['openbookV2Markets'],
groupConfig['perpMarkets'],
);
}
@ -117,11 +146,13 @@ export class Id {
(group) => group.publicKey === groupPk.toString(),
);
// todo-pan: api won't return obv2 stuff yet
return new Id(
groupConfig.cluster as Cluster,
groupConfig.name,
groupConfig.publicKey,
groupConfig.serum3ProgramId,
groupConfig.openbookV2ProgramId,
groupConfig.mangoProgramId,
groupConfig.tokens.flatMap((t) =>
t.banks.map((b) => ({
@ -151,6 +182,12 @@ export class Id {
marketExternal: s.serumMarketExternal,
active: s.active,
})),
groupConfig.openbookV2Markets.map((s) => ({
name: s.name,
publicKey: s.publicKey,
marketExternal: s.openbookMarketExternal,
active: s.active,
})),
groupConfig.perpMarkets.map((p) => ({
name: p.name,
publicKey: p.publicKey,

View File

@ -7,6 +7,7 @@ export * from './accounts/bank';
export * from './accounts/mangoAccount';
export * from './accounts/oracle';
export * from './accounts/perp';
export * from './accounts/openbookV2';
export {
Serum3Market,
Serum3OrderType,

File diff suppressed because it is too large Load Diff

View File

@ -1,10 +1,12 @@
import { AnchorProvider } from '@coral-xyz/anchor';
import { AnchorProvider, Wallet } from '@coral-xyz/anchor';
import {
AddressLookupTableAccount,
Keypair,
MessageV0,
PublicKey,
Signer,
SystemProgram,
Transaction,
TransactionInstruction,
VersionedTransaction,
} from '@solana/web3.js';
@ -209,6 +211,25 @@ export async function buildVersionedTx(
return vTx;
}
export class EmptyWallet implements Wallet {
constructor(readonly payer: Keypair) {}
async signTransaction<T extends Transaction | VersionedTransaction>(
tx: T,
): Promise<T> {
return tx;
}
async signAllTransactions<T extends Transaction | VersionedTransaction>(
txs: T[],
): Promise<T[]> {
return txs;
}
get publicKey(): PublicKey {
return this.payer.publicKey;
}
}
///
/// ts extension
///

View File

@ -8,7 +8,7 @@
"resolveJsonModule": true,
"skipLibCheck": true,
"strictNullChecks": true,
"target": "esnext",
"target": "esnext"
},
"ts-node": {
// these options are overrides used only by ts-node
@ -17,7 +17,6 @@
"module": "commonjs"
}
},
"include": [
"ts/client/src"
]
}
"include": ["ts/client/src"],
"exclude": ["ts/client/scripts"]
}

View File

@ -54,15 +54,14 @@
resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9"
integrity sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==
"@coral-xyz/anchor@^0.26.0", "@coral-xyz/anchor@^0.28.1-beta.2":
version "0.28.1-beta.2"
resolved "https://registry.yarnpkg.com/@coral-xyz/anchor/-/anchor-0.28.1-beta.2.tgz#4ddd4b2b66af04407be47cf9524147793ec514a0"
integrity sha512-xreUcOFF8+IQKWOBUrDKJbIw2ftpRVybFlEPVrbSlOBCbreCWrQ5754Gt9cHIcuBDAzearCDiBqzsGQdNgPJiw==
"@coral-xyz/anchor@^0.26.0", "@coral-xyz/anchor@^0.28.1-beta.2", "@coral-xyz/anchor@^0.29.0":
version "0.29.0"
resolved "https://registry.yarnpkg.com/@coral-xyz/anchor/-/anchor-0.29.0.tgz#bd0be95bedfb30a381c3e676e5926124c310ff12"
integrity sha512-eny6QNG0WOwqV0zQ7cs/b1tIuzZGmP7U7EcH+ogt4Gdbl8HDmIYVMh/9aTmYZPaFWjtUaI8qSn73uYEXWfATdA==
dependencies:
"@coral-xyz/borsh" "^0.28.0"
"@coral-xyz/borsh" "^0.29.0"
"@noble/hashes" "^1.3.1"
"@solana/web3.js" "^1.68.0"
base64-js "^1.5.1"
bn.js "^5.1.2"
bs58 "^4.0.1"
buffer-layout "^1.2.2"
@ -75,10 +74,10 @@
superstruct "^0.15.4"
toml "^3.0.0"
"@coral-xyz/borsh@^0.28.0":
version "0.28.0"
resolved "https://registry.yarnpkg.com/@coral-xyz/borsh/-/borsh-0.28.0.tgz#fa368a2f2475bbf6f828f4657f40a52102e02b6d"
integrity sha512-/u1VTzw7XooK7rqeD7JLUSwOyRSesPUk0U37BV9zK0axJc1q0nRbKFGFLYCQ16OtdOJTTwGfGp11Lx9B45bRCQ==
"@coral-xyz/borsh@^0.29.0":
version "0.29.0"
resolved "https://registry.yarnpkg.com/@coral-xyz/borsh/-/borsh-0.29.0.tgz#79f7045df2ef66da8006d47f5399c7190363e71f"
integrity sha512-s7VFVa3a0oqpkuRloWVPdCK7hMbAMY270geZOGfCnaqexrP5dTIpbEHL33req6IYPPJ0hYa71cdvJ1h6V55/oQ==
dependencies:
bn.js "^5.1.2"
buffer-layout "^1.2.0"
@ -169,11 +168,16 @@
dependencies:
"@noble/hashes" "1.3.3"
"@noble/hashes@1.3.3", "@noble/hashes@^1.3.1", "@noble/hashes@^1.3.2":
"@noble/hashes@1.3.3":
version "1.3.3"
resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.3.tgz#39908da56a4adc270147bb07968bf3b16cfe1699"
integrity sha512-V7/fPHgl+jsVPXqqeOzT8egNj2iBIVt+ECeMMG8TdcnTikP3oaBtUVqpT/gYCR68aEBJSF+XbYUxStjbFMqIIA==
"@noble/hashes@^1.3.1", "@noble/hashes@^1.3.2", "@noble/hashes@^1.3.3":
version "1.4.0"
resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.4.0.tgz#45814aa329f30e4fe0ba49426f49dfccdd066426"
integrity sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==
"@nodelib/fs.scandir@2.1.5":
version "2.1.5"
resolved "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz"
@ -195,6 +199,16 @@
"@nodelib/fs.scandir" "2.1.5"
fastq "^1.6.0"
"@openbook-dex/openbook-v2@^0.1.2":
version "0.1.10"
resolved "https://registry.yarnpkg.com/@openbook-dex/openbook-v2/-/openbook-v2-0.1.10.tgz#8c7ba941d9d15376726864a0cfffd3561ed4778f"
integrity sha512-k462N5YwCPxWGWNxUGPwXxhdnObkiQKKhgzAk58S2nekkqeimChM2ljUk3Zd/qPOIgR4mtfVDvoMHrxJ0H6R9g==
dependencies:
"@coral-xyz/anchor" "^0.28.1-beta.2"
"@solana/spl-token" "0.3.8"
"@solana/web3.js" "^1.77.3"
big.js "^6.2.1"
"@project-serum/anchor@^0.11.1":
version "0.11.1"
resolved "https://registry.npmjs.org/@project-serum/anchor/-/anchor-0.11.1.tgz"
@ -301,6 +315,15 @@
"@solana/buffer-layout-utils" "^0.2.0"
buffer "^6.0.3"
"@solana/spl-token@0.3.8":
version "0.3.8"
resolved "https://registry.yarnpkg.com/@solana/spl-token/-/spl-token-0.3.8.tgz#8e9515ea876e40a4cc1040af865f61fc51d27edf"
integrity sha512-ogwGDcunP9Lkj+9CODOWMiVJEdRtqHAtX2rWF62KxnnSWtMZtV9rDhTrZFshiyJmxDnRL/1nKE1yJHg4jjs3gg==
dependencies:
"@solana/buffer-layout" "^4.0.0"
"@solana/buffer-layout-utils" "^0.2.0"
buffer "^6.0.3"
"@solana/spl-token@^0.1.6":
version "0.1.8"
resolved "https://registry.npmjs.org/@solana/spl-token/-/spl-token-0.1.8.tgz"
@ -313,14 +336,14 @@
buffer-layout "^1.2.0"
dotenv "10.0.0"
"@solana/web3.js@^1.17.0", "@solana/web3.js@^1.21.0", "@solana/web3.js@^1.22.0", "@solana/web3.js@^1.32.0", "@solana/web3.js@^1.36.0", "@solana/web3.js@^1.68.0", "@solana/web3.js@^1.78.2", "@solana/web3.js@^1.88.0":
version "1.88.0"
resolved "https://registry.yarnpkg.com/@solana/web3.js/-/web3.js-1.88.0.tgz#24e1482f63ac54914430b4ce5ab36eaf433ecdb8"
integrity sha512-E4BdfB0HZpb66OPFhIzPApNE2tG75Mc6XKIoeymUkx/IV+USSYuxDX29sjgE/KGNYxggrOf4YuYnRMI6UiPL8w==
"@solana/web3.js@^1.17.0", "@solana/web3.js@^1.21.0", "@solana/web3.js@^1.22.0", "@solana/web3.js@^1.32.0", "@solana/web3.js@^1.36.0", "@solana/web3.js@^1.68.0", "@solana/web3.js@^1.77.3", "@solana/web3.js@^1.78.2", "@solana/web3.js@^1.88.0":
version "1.91.4"
resolved "https://registry.yarnpkg.com/@solana/web3.js/-/web3.js-1.91.4.tgz#b80295ce72aa125930dfc5b41b4b4e3f85fd87fa"
integrity sha512-zconqecIcBqEF6JiM4xYF865Xc4aas+iWK5qnu7nwKPq9ilRYcn+2GiwpYXqUqqBUe0XCO17w18KO0F8h+QATg==
dependencies:
"@babel/runtime" "^7.23.4"
"@noble/curves" "^1.2.0"
"@noble/hashes" "^1.3.2"
"@noble/hashes" "^1.3.3"
"@solana/buffer-layout" "^4.0.1"
agentkeepalive "^4.5.0"
bigint-buffer "^1.1.5"
@ -694,9 +717,9 @@ base64-js@^1.3.1, base64-js@^1.5.1:
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
big.js@^6.1.1:
big.js@^6.1.1, big.js@^6.2.1:
version "6.2.1"
resolved "https://registry.npmjs.org/big.js/-/big.js-6.2.1.tgz"
resolved "https://registry.yarnpkg.com/big.js/-/big.js-6.2.1.tgz#7205ce763efb17c2e41f26f121c420c6a7c2744f"
integrity sha512-bCtHMwL9LeDIozFn+oNhhFoq+yQ3BNdnsLSASUxLciOb1vgvpHsIO1dsENiGMgbb4SkP5TrzWzRiLddn8ahVOQ==
bigint-buffer@^1.1.5: