From 3245a68e927f9b7e6e68c2f07e9a718c4577c41e Mon Sep 17 00:00:00 2001 From: bekauz Date: Fri, 18 Oct 2024 21:19:26 +0200 Subject: [PATCH] osmo gamm lper (#72) * init osmosis lper * clippy * wip: osmosis lper integration into valence services; testing suite init * update optimize.sh * adding osmosis-utils package * wip: osmo uploading contracts debugging * fix unit test setup * removing duplicate pool_id field from configs * providing two sided liquidity * extend test suite * single sided liquidity provision * add OsmosisPoolType * move osmo message related utils to osmosis-utils package * add pool_types mod * concentrated liquidity mod * add a shift cl pool helper * cosmwasm pool test setup * wip: transmuter * remove cl & cw osmo lper logic * rename to osmosis-gamm-lper * cleanup; add lp token transfer from input -> output acc * add post-lp transfer to single side lp * cleanup osmo gamm lper * add spot price range validation & tests * add pool denom validation * readme * adjust cargo.toml * rearrange utils * remove valence_service_integration & balancer mods; toml updates * rename ActionMsgs; schemagen * ci * merge --- Cargo.lock | 239 +++++- Cargo.toml | 6 +- contracts/accounts/base_account/Cargo.toml | 2 +- contracts/authorization/src/tests/helpers.rs | 2 +- .../src/astroport_native.rs | 2 +- .../osmosis-gamm-lper/.cargo/config.toml | 3 + .../services/osmosis-gamm-lper/Cargo.toml | 31 + .../services/osmosis-gamm-lper/README.md | 1 + .../osmosis-gamm-lper/src/bin/schema.rs | 12 + .../bin/schema/valence-osmosis-gamm-lper.json | 800 ++++++++++++++++++ .../osmosis-gamm-lper/src/contract.rs | 266 ++++++ .../services/osmosis-gamm-lper/src/lib.rs | 4 + .../services/osmosis-gamm-lper/src/msg.rs | 166 ++++ .../osmosis-gamm-lper/src/testing/mod.rs | 4 + .../src/testing/test_suite.rs | 197 +++++ .../osmosis-gamm-lper/src/testing/tests.rs | 129 +++ devtools/optimize.sh | 1 - local-interchaintest/examples/polytone.rs | 2 +- .../src/utils/authorization.rs | 2 +- local-interchaintest/src/utils/manager.rs | 2 +- packages/osmosis-utils/Cargo.toml | 21 + packages/osmosis-utils/src/lib.rs | 4 + packages/osmosis-utils/src/suite.rs | 132 +++ packages/osmosis-utils/src/utils.rs | 67 ++ workflow-manager/src/domain/mod.rs | 2 +- 25 files changed, 2051 insertions(+), 46 deletions(-) create mode 100644 contracts/services/osmosis-gamm-lper/.cargo/config.toml create mode 100644 contracts/services/osmosis-gamm-lper/Cargo.toml create mode 100644 contracts/services/osmosis-gamm-lper/README.md create mode 100644 contracts/services/osmosis-gamm-lper/src/bin/schema.rs create mode 100644 contracts/services/osmosis-gamm-lper/src/bin/schema/valence-osmosis-gamm-lper.json create mode 100644 contracts/services/osmosis-gamm-lper/src/contract.rs create mode 100644 contracts/services/osmosis-gamm-lper/src/lib.rs create mode 100644 contracts/services/osmosis-gamm-lper/src/msg.rs create mode 100644 contracts/services/osmosis-gamm-lper/src/testing/mod.rs create mode 100644 contracts/services/osmosis-gamm-lper/src/testing/test_suite.rs create mode 100644 contracts/services/osmosis-gamm-lper/src/testing/tests.rs create mode 100644 packages/osmosis-utils/Cargo.toml create mode 100644 packages/osmosis-utils/src/lib.rs create mode 100644 packages/osmosis-utils/src/suite.rs create mode 100644 packages/osmosis-utils/src/utils.rs diff --git a/Cargo.lock b/Cargo.lock index cd05b24d..f2716207 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -431,6 +431,29 @@ dependencies = [ "which", ] +[[package]] +name = "bindgen" +version = "0.69.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088" +dependencies = [ + "bitflags 2.6.0", + "cexpr", + "clang-sys", + "itertools 0.12.1", + "lazy_static", + "lazycell", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn 2.0.79", + "which", +] + [[package]] name = "bip32" version = "0.5.2" @@ -553,9 +576,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.1.28" +version = "1.1.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e80e3b6a3ab07840e1cae9b0666a63970dc28e8ed5ffbcdacbfc760c281bfc1" +checksum = "b16803a61b81d9eabb7eae2588776c4c1e584b738ede45fdbb4c972cec1e9945" dependencies = [ "shlex", ] @@ -1380,18 +1403,18 @@ dependencies = [ [[package]] name = "derive_builder" -version = "0.20.1" +version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd33f37ee6a119146a1781d3356a7c26028f83d779b2e04ecd45fdc75c76877b" +checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947" dependencies = [ "derive_builder_macro", ] [[package]] name = "derive_builder_core" -version = "0.20.1" +version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7431fa049613920234f22c47fdc33e6cf3ee83067091ea4277a3f8c4587aae38" +checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" dependencies = [ "darling", "proc-macro2", @@ -1401,9 +1424,9 @@ dependencies = [ [[package]] name = "derive_builder_macro" -version = "0.20.1" +version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4abae7035bf79b9877b779505d8cf3749285b80c43941eda66604841889451dc" +checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" dependencies = [ "derive_builder_core", "syn 2.0.79", @@ -2285,9 +2308,9 @@ checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" [[package]] name = "js-sys" -version = "0.3.70" +version = "0.3.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a" +checksum = "6a88f1bda2bd75b0452a14784937d796722fdebfe50df998aeb3f0b7603019a9" dependencies = [ "wasm-bindgen", ] @@ -2558,7 +2581,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "80526b0e02d9b8e6cc3790d0252161def3fb4d025975ec600e561bce315d8468" dependencies = [ "base64 0.21.7", - "bindgen", + "bindgen 0.60.1", "cosmrs 0.15.0", "cosmwasm-std 2.1.4", "hex", @@ -2772,6 +2795,82 @@ version = "6.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2355d85b9a3786f481747ced0e0ff2ba35213a1f9bd406ed906554d7af805a1" +[[package]] +name = "osmosis-std" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca66dca7e8c9b11b995cd41a44c038134ccca4469894d663d8a9452d6e716241" +dependencies = [ + "chrono", + "cosmwasm-std 1.5.8", + "osmosis-std-derive 0.20.1", + "prost 0.12.6", + "prost-types 0.12.6", + "schemars", + "serde", + "serde-cw-value", +] + +[[package]] +name = "osmosis-std" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd621cc2f26474c6fb689ccc114dc0d8b53369a322f1fa5f24802f3de3d3def3" +dependencies = [ + "chrono", + "cosmwasm-std 2.1.4", + "osmosis-std-derive 0.26.0", + "prost 0.13.3", + "prost-types 0.13.3", + "schemars", + "serde", + "serde-cw-value", +] + +[[package]] +name = "osmosis-std-derive" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5ebdfd1bc8ed04db596e110c6baa9b174b04f6ed1ec22c666ddc5cb3fa91bd7" +dependencies = [ + "itertools 0.10.5", + "proc-macro2", + "prost-types 0.11.9", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "osmosis-std-derive" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b0240fd030a4bbc79fa6cbea0b3eb0260a4b79075ebc039b93e2652bff8655b" +dependencies = [ + "itertools 0.10.5", + "proc-macro2", + "prost-types 0.11.9", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "osmosis-test-tube" +version = "25.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eb35dcc9adc1b39e23dfae07c9f04a60187fde57a52b7762434ea6548581a1a" +dependencies = [ + "base64 0.21.7", + "bindgen 0.69.5", + "cosmrs 0.15.0", + "cosmwasm-std 1.5.8", + "osmosis-std 0.25.0", + "prost 0.12.6", + "serde", + "serde_json", + "test-tube", + "thiserror", +] + [[package]] name = "overload" version = "0.1.1" @@ -2821,9 +2920,9 @@ checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" [[package]] name = "pathdiff" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" +checksum = "d61c5ce1153ab5b689d0c074c4e7fc613e942dfb7dd9eea5ab202d2ad91fe361" [[package]] name = "pbkdf2" @@ -2876,9 +2975,9 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pest" -version = "2.7.13" +version = "2.7.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdbef9d1d47087a895abd220ed25eb4ad973a5e26f6a4367b038c25e28dfc2d9" +checksum = "879952a81a83930934cbf1786752d6dedc3b1f29e8f8fb2ad1d0a36f377cf442" dependencies = [ "memchr", "thiserror", @@ -2887,9 +2986,9 @@ dependencies = [ [[package]] name = "pest_derive" -version = "2.7.13" +version = "2.7.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d3a6e3394ec80feb3b6393c725571754c6188490265c61aaf260810d6b95aa0" +checksum = "d214365f632b123a47fd913301e14c946c61d1c183ee245fa76eb752e59a02dd" dependencies = [ "pest", "pest_generator", @@ -2897,9 +2996,9 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.7.13" +version = "2.7.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94429506bde1ca69d1b5601962c73f4172ab4726571a59ea95931218cb0e930e" +checksum = "eb55586734301717aea2ac313f50b2eb8f60d2fc3dc01d190eefa2e625f60c4e" dependencies = [ "pest", "pest_meta", @@ -2910,9 +3009,9 @@ dependencies = [ [[package]] name = "pest_meta" -version = "2.7.13" +version = "2.7.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac8a071862e93690b6e34e9a5fb8e33ff3734473ac0245b27232222c4906a33f" +checksum = "b75da2a70cf4d9cb76833c990ac9cd3923c9a8905a8929789ce347c84564d03d" dependencies = [ "once_cell", "pest", @@ -3037,6 +3136,16 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "prettyplease" +version = "0.2.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479cf940fbbb3426c32c5d5176f62ad57549a0bb84773423ba8be9d089f5faba" +dependencies = [ + "proc-macro2", + "syn 2.0.79", +] + [[package]] name = "primeorder" version = "0.13.6" @@ -3164,6 +3273,15 @@ dependencies = [ "prost 0.12.6", ] +[[package]] +name = "prost-types" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4759aa0d3a6232fb8dbdb97b61de2c20047c68aca932c7ed76da9d788508d670" +dependencies = [ + "prost 0.13.3", +] + [[package]] name = "protobuf" version = "2.28.0" @@ -3538,9 +3656,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" +checksum = "0e819f2bc632f285be6d7cd36e25940d45b2391dd6d9b939e79de557f7014248" [[package]] name = "ryu" @@ -4212,6 +4330,22 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "test-tube" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "804bb9bda992b6cda6f883e7973cb999d4da90d21257fb918d6a693407148681" +dependencies = [ + "base64 0.21.7", + "cosmrs 0.15.0", + "cosmwasm-std 1.5.8", + "osmosis-std 0.25.0", + "prost 0.12.6", + "serde", + "serde_json", + "thiserror", +] + [[package]] name = "test-tube-ntrn" version = "0.1.3" @@ -4833,6 +4967,37 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "valence-osmosis-gamm-lper" +version = "0.1.0" +dependencies = [ + "cosmwasm-schema 2.1.4", + "cosmwasm-std 1.5.8", + "cosmwasm-std 2.1.4", + "cw-ownable", + "cw20 2.0.0", + "osmosis-std 0.26.0", + "osmosis-test-tube", + "valence-account-utils", + "valence-macros", + "valence-osmosis-utils", + "valence-service-base", + "valence-service-utils", +] + +[[package]] +name = "valence-osmosis-utils" +version = "0.1.0" +dependencies = [ + "cosmwasm-schema 2.1.4", + "cosmwasm-std 1.5.8", + "cosmwasm-std 2.1.4", + "osmosis-std 0.26.0", + "osmosis-test-tube", + "valence-account-utils", + "valence-service-utils", +] + [[package]] name = "valence-polytone-utils" version = "0.1.0" @@ -5127,9 +5292,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.93" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5" +checksum = "128d1e363af62632b8eb57219c8fd7877144af57558fb2ef0368d0087bddeb2e" dependencies = [ "cfg-if", "once_cell", @@ -5138,9 +5303,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.93" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b" +checksum = "cb6dd4d3ca0ddffd1dd1c9c04f94b868c37ff5fac97c30b97cff2d74fce3a358" dependencies = [ "bumpalo", "log", @@ -5153,9 +5318,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.43" +version = "0.4.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61e9300f63a621e96ed275155c108eb6f843b6a26d053f122ab69724559dc8ed" +checksum = "cc7ec4f8827a71586374db3e87abdb5a2bb3a15afed140221307c3ec06b1f63b" dependencies = [ "cfg-if", "js-sys", @@ -5165,9 +5330,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.93" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf" +checksum = "e79384be7f8f5a9dd5d7167216f022090cf1f9ec128e6e6a482a2cb5c5422c56" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -5175,9 +5340,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.93" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836" +checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68" dependencies = [ "proc-macro2", "quote", @@ -5188,15 +5353,15 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.93" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484" +checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d" [[package]] name = "web-sys" -version = "0.3.70" +version = "0.3.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26fdeaafd9bd129f65e7c031593c24d62186301e0c72c8978fa1678be7d532c0" +checksum = "f6488b90108c040df0fe62fa815cbdee25124641df01814dd7282749234c6112" dependencies = [ "js-sys", "wasm-bindgen", diff --git a/Cargo.toml b/Cargo.toml index 9eb11fe8..269d8ab9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,7 +32,7 @@ overflow-checks = true [workspace.dependencies] anyhow = "1.0.86" -cosmwasm-std = { version = "2.1.3", features = ["cosmwasm_1_4"] } +cosmwasm-std = { version = "2.1.3" } cosmwasm-schema = "2.1.3" cw-denom = { package = "cw-denom", git = "https://github.com/DA0-DA0/dao-contracts", branch = "cw-std-2" } cw-ownable = "2.0.0" @@ -49,6 +49,7 @@ serde = { version = "1.0.207", default-features = false, features = [ serde_json = "1.0.125" sha2 = "0.10.8" thiserror = "1.0.63" +osmosis-std = "0.26.0" # our contracts valence-authorization = { path = "contracts/authorization", features = ["library"] } @@ -62,10 +63,12 @@ valence-astroport-lper = { path = "contracts/services/astroport-lper", valence-forwarder-service = { path = "contracts/services/forwarder", features = ["library"] } valence-astroport-withdrawer = { path = "contracts/services/astroport-withdrawer", features = ["library"] } valence-reverse-splitter-service = { path = "contracts/services/reverse-splitter", features = ["library"] } +valence-osmosis-gamm-lper = { path = "contracts/services/osmosis-gamm-lper", features = ["library"] } # our packages valence-account-utils = { path = "packages/account-utils" } valence-astroport-utils = { path = "packages/astroport-utils" } +valence-osmosis-utils = { path = "packages/osmosis-utils" } valence-authorization-utils = { path = "packages/authorization-utils" } valence-macros = { path = "packages/valence-macros" } valence-polytone-utils = { path = "packages/polytone-utils" } @@ -83,3 +86,4 @@ hex = "0.4.3" margined-neutron-std = "4.2.0" neutron-test-tube = "4.2.0" tokio = "1.40.0" +osmosis-test-tube = "25.0.0" diff --git a/contracts/accounts/base_account/Cargo.toml b/contracts/accounts/base_account/Cargo.toml index 0af016bf..092fdd9f 100644 --- a/contracts/accounts/base_account/Cargo.toml +++ b/contracts/accounts/base_account/Cargo.toml @@ -22,7 +22,7 @@ optimize = """docker run --rm -v "$(pwd)":/code \ [dependencies] cosmwasm-schema = { workspace = true } -cosmwasm-std = { workspace = true } +cosmwasm-std = { workspace = true, features = ["stargate"] } cw-ownable = { workspace = true } cw-storage-plus = { workspace = true } diff --git a/contracts/authorization/src/tests/helpers.rs b/contracts/authorization/src/tests/helpers.rs index e03df027..874040eb 100644 --- a/contracts/authorization/src/tests/helpers.rs +++ b/contracts/authorization/src/tests/helpers.rs @@ -118,7 +118,7 @@ pub fn store_and_instantiate_authorization_with_processor_contract( let salt = hex::encode("authorization"); let predicted_address = extended_wasm .query_build_address( - hex::encode(&checksum), + hex::encode(checksum), signer.address().to_string(), salt.clone(), ) diff --git a/contracts/services/astroport-withdrawer/src/astroport_native.rs b/contracts/services/astroport-withdrawer/src/astroport_native.rs index 76fded89..f6ec4dc2 100644 --- a/contracts/services/astroport-withdrawer/src/astroport_native.rs +++ b/contracts/services/astroport-withdrawer/src/astroport_native.rs @@ -20,7 +20,7 @@ pub fn create_withdraw_liquidity_msgs( let token = query_liquidity_token(deps, cfg)?; // Query the balance of the account that is going to withdraw - let balance = deps.querier.query_balance(&cfg.input_addr, &token)?; + let balance = deps.querier.query_balance(&cfg.input_addr, token)?; if balance.amount.is_zero() { return Err(ServiceError::ExecutionError( "Nothing to withdraw".to_string(), diff --git a/contracts/services/osmosis-gamm-lper/.cargo/config.toml b/contracts/services/osmosis-gamm-lper/.cargo/config.toml new file mode 100644 index 00000000..5f6aa466 --- /dev/null +++ b/contracts/services/osmosis-gamm-lper/.cargo/config.toml @@ -0,0 +1,3 @@ +[alias] +wasm = "build --release --lib --target wasm32-unknown-unknown" +schema = "run --bin schema" diff --git a/contracts/services/osmosis-gamm-lper/Cargo.toml b/contracts/services/osmosis-gamm-lper/Cargo.toml new file mode 100644 index 00000000..4bdc6e82 --- /dev/null +++ b/contracts/services/osmosis-gamm-lper/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "valence-osmosis-gamm-lper" +authors = { workspace = true } +edition = { workspace = true } +license = { workspace = true } +version = { workspace = true } +repository = { workspace = true } + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +# use library feature to disable all instantiate/execute/query exports +library = [] + +[dependencies] +cosmwasm-std = { workspace = true } +cosmwasm-schema = { workspace = true } +cw-ownable = { workspace = true } +valence-macros = { workspace = true } +valence-service-utils = { workspace = true } +valence-service-base = { workspace = true } +osmosis-std = { workspace = true } +valence-account-utils = { workspace = true } +valence-osmosis-utils = { workspace = true } + +[dev-dependencies] +cosmwasm-std-old = { package = "cosmwasm-std", version = "1.5.7" } +cw20 = { workspace = true } +osmosis-test-tube = { workspace = true } +valence-osmosis-utils = { workspace = true, features = ["testing"] } diff --git a/contracts/services/osmosis-gamm-lper/README.md b/contracts/services/osmosis-gamm-lper/README.md new file mode 100644 index 00000000..aa8ab273 --- /dev/null +++ b/contracts/services/osmosis-gamm-lper/README.md @@ -0,0 +1 @@ +# Osmosis GAMM liquidity provider service diff --git a/contracts/services/osmosis-gamm-lper/src/bin/schema.rs b/contracts/services/osmosis-gamm-lper/src/bin/schema.rs new file mode 100644 index 00000000..3417c4f8 --- /dev/null +++ b/contracts/services/osmosis-gamm-lper/src/bin/schema.rs @@ -0,0 +1,12 @@ +use cosmwasm_schema::write_api; + +use valence_osmosis_gamm_lper::msg::{ActionMsgs, QueryMsg, ServiceConfig, ServiceConfigUpdate}; +use valence_service_utils::msg::{ExecuteMsg, InstantiateMsg}; + +fn main() { + write_api! { + instantiate: InstantiateMsg, + execute: ExecuteMsg, + query: QueryMsg, + } +} diff --git a/contracts/services/osmosis-gamm-lper/src/bin/schema/valence-osmosis-gamm-lper.json b/contracts/services/osmosis-gamm-lper/src/bin/schema/valence-osmosis-gamm-lper.json new file mode 100644 index 00000000..03d54b70 --- /dev/null +++ b/contracts/services/osmosis-gamm-lper/src/bin/schema/valence-osmosis-gamm-lper.json @@ -0,0 +1,800 @@ +{ + "contract_name": "valence-osmosis-gamm-lper", + "contract_version": "0.1.0", + "idl_version": "1.0.0", + "instantiate": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "InstantiateMsg", + "type": "object", + "required": [ + "config", + "owner", + "processor" + ], + "properties": { + "config": { + "$ref": "#/definitions/ServiceConfig" + }, + "owner": { + "type": "string" + }, + "processor": { + "type": "string" + } + }, + "additionalProperties": false, + "definitions": { + "LiquidityProviderConfig": { + "type": "object", + "required": [ + "pool_asset_1", + "pool_asset_2", + "pool_id" + ], + "properties": { + "pool_asset_1": { + "type": "string" + }, + "pool_asset_2": { + "type": "string" + }, + "pool_id": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + "ServiceAccountType": { + "description": "An account type that is used in the service configs It can either be an Id or Addr The config that will be passed to the service must be of Addr veriant", + "oneOf": [ + { + "type": "object", + "required": [ + "|service_account_addr|" + ], + "properties": { + "|service_account_addr|": { + "type": "string" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "|account_id|" + ], + "properties": { + "|account_id|": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "|service_id|" + ], + "properties": { + "|service_id|": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + ] + }, + "ServiceConfig": { + "type": "object", + "required": [ + "input_addr", + "lp_config", + "output_addr" + ], + "properties": { + "input_addr": { + "$ref": "#/definitions/ServiceAccountType" + }, + "lp_config": { + "$ref": "#/definitions/LiquidityProviderConfig" + }, + "output_addr": { + "$ref": "#/definitions/ServiceAccountType" + } + }, + "additionalProperties": false + } + } + }, + "execute": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ExecuteMsg", + "oneOf": [ + { + "type": "object", + "required": [ + "process_action" + ], + "properties": { + "process_action": { + "$ref": "#/definitions/ActionMsgs" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "update_config" + ], + "properties": { + "update_config": { + "type": "object", + "required": [ + "new_config" + ], + "properties": { + "new_config": { + "$ref": "#/definitions/ServiceConfigUpdate" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "update_processor" + ], + "properties": { + "update_processor": { + "type": "object", + "required": [ + "processor" + ], + "properties": { + "processor": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Update the contract's ownership. The `action` to be provided can be either to propose transferring ownership to an account, accept a pending ownership transfer, or renounce the ownership permanently.", + "type": "object", + "required": [ + "update_ownership" + ], + "properties": { + "update_ownership": { + "$ref": "#/definitions/Action" + } + }, + "additionalProperties": false + } + ], + "definitions": { + "Action": { + "description": "Actions that can be taken to alter the contract's ownership", + "oneOf": [ + { + "description": "Propose to transfer the contract's ownership to another account, optionally with an expiry time.\n\nCan only be called by the contract's current owner.\n\nAny existing pending ownership transfer is overwritten.", + "type": "object", + "required": [ + "transfer_ownership" + ], + "properties": { + "transfer_ownership": { + "type": "object", + "required": [ + "new_owner" + ], + "properties": { + "expiry": { + "anyOf": [ + { + "$ref": "#/definitions/Expiration" + }, + { + "type": "null" + } + ] + }, + "new_owner": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Accept the pending ownership transfer.\n\nCan only be called by the pending owner.", + "type": "string", + "enum": [ + "accept_ownership" + ] + }, + { + "description": "Give up the contract's ownership and the possibility of appointing a new owner.\n\nCan only be invoked by the contract's current owner.\n\nAny existing pending ownership transfer is canceled.", + "type": "string", + "enum": [ + "renounce_ownership" + ] + } + ] + }, + "ActionMsgs": { + "oneOf": [ + { + "type": "object", + "required": [ + "provide_double_sided_liquidity" + ], + "properties": { + "provide_double_sided_liquidity": { + "type": "object", + "properties": { + "expected_spot_price": { + "anyOf": [ + { + "$ref": "#/definitions/DecimalRange" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "provide_single_sided_liquidity" + ], + "properties": { + "provide_single_sided_liquidity": { + "type": "object", + "required": [ + "asset", + "limit" + ], + "properties": { + "asset": { + "type": "string" + }, + "expected_spot_price": { + "anyOf": [ + { + "$ref": "#/definitions/DecimalRange" + }, + { + "type": "null" + } + ] + }, + "limit": { + "$ref": "#/definitions/Uint128" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "DecimalRange": { + "type": "object", + "required": [ + "max", + "min" + ], + "properties": { + "max": { + "$ref": "#/definitions/Decimal" + }, + "min": { + "$ref": "#/definitions/Decimal" + } + }, + "additionalProperties": false + }, + "Expiration": { + "description": "Expiration represents a point in time when some event happens. It can compare with a BlockInfo and will return is_expired() == true once the condition is hit (and for every block in the future)", + "oneOf": [ + { + "description": "AtHeight will expire when `env.block.height` >= height", + "type": "object", + "required": [ + "at_height" + ], + "properties": { + "at_height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + { + "description": "AtTime will expire when `env.block.time` >= time", + "type": "object", + "required": [ + "at_time" + ], + "properties": { + "at_time": { + "$ref": "#/definitions/Timestamp" + } + }, + "additionalProperties": false + }, + { + "description": "Never will never expire. Used to express the empty variant", + "type": "object", + "required": [ + "never" + ], + "properties": { + "never": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "LiquidityProviderConfig": { + "type": "object", + "required": [ + "pool_asset_1", + "pool_asset_2", + "pool_id" + ], + "properties": { + "pool_asset_1": { + "type": "string" + }, + "pool_asset_2": { + "type": "string" + }, + "pool_id": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + "ServiceAccountType": { + "description": "An account type that is used in the service configs It can either be an Id or Addr The config that will be passed to the service must be of Addr veriant", + "oneOf": [ + { + "type": "object", + "required": [ + "|service_account_addr|" + ], + "properties": { + "|service_account_addr|": { + "type": "string" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "|account_id|" + ], + "properties": { + "|account_id|": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "|service_id|" + ], + "properties": { + "|service_id|": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + ] + }, + "ServiceConfigUpdate": { + "type": "object", + "properties": { + "input_addr": { + "anyOf": [ + { + "$ref": "#/definitions/ServiceAccountType" + }, + { + "type": "null" + } + ] + }, + "lp_config": { + "anyOf": [ + { + "$ref": "#/definitions/LiquidityProviderConfig" + }, + { + "type": "null" + } + ] + }, + "output_addr": { + "anyOf": [ + { + "$ref": "#/definitions/ServiceAccountType" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + "Timestamp": { + "description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```", + "allOf": [ + { + "$ref": "#/definitions/Uint64" + } + ] + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + }, + "Uint64": { + "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```", + "type": "string" + } + } + }, + "query": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "QueryMsg", + "oneOf": [ + { + "description": "Query to get the processor address.", + "type": "object", + "required": [ + "get_processor" + ], + "properties": { + "get_processor": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Query to get the service configuration.", + "type": "object", + "required": [ + "get_service_config" + ], + "properties": { + "get_service_config": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "get_raw_service_config" + ], + "properties": { + "get_raw_service_config": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Query the contract's ownership information", + "type": "object", + "required": [ + "ownership" + ], + "properties": { + "ownership": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "migrate": null, + "sudo": null, + "responses": { + "get_processor": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Addr", + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + }, + "get_raw_service_config": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ServiceConfig", + "type": "object", + "required": [ + "input_addr", + "lp_config", + "output_addr" + ], + "properties": { + "input_addr": { + "$ref": "#/definitions/ServiceAccountType" + }, + "lp_config": { + "$ref": "#/definitions/LiquidityProviderConfig" + }, + "output_addr": { + "$ref": "#/definitions/ServiceAccountType" + } + }, + "additionalProperties": false, + "definitions": { + "LiquidityProviderConfig": { + "type": "object", + "required": [ + "pool_asset_1", + "pool_asset_2", + "pool_id" + ], + "properties": { + "pool_asset_1": { + "type": "string" + }, + "pool_asset_2": { + "type": "string" + }, + "pool_id": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + "ServiceAccountType": { + "description": "An account type that is used in the service configs It can either be an Id or Addr The config that will be passed to the service must be of Addr veriant", + "oneOf": [ + { + "type": "object", + "required": [ + "|service_account_addr|" + ], + "properties": { + "|service_account_addr|": { + "type": "string" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "|account_id|" + ], + "properties": { + "|account_id|": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "|service_id|" + ], + "properties": { + "|service_id|": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + ] + } + } + }, + "get_service_config": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Config", + "description": "Validated service configuration", + "type": "object", + "required": [ + "input_addr", + "lp_config", + "output_addr" + ], + "properties": { + "input_addr": { + "$ref": "#/definitions/Addr" + }, + "lp_config": { + "$ref": "#/definitions/LiquidityProviderConfig" + }, + "output_addr": { + "$ref": "#/definitions/Addr" + } + }, + "additionalProperties": false, + "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + }, + "LiquidityProviderConfig": { + "type": "object", + "required": [ + "pool_asset_1", + "pool_asset_2", + "pool_id" + ], + "properties": { + "pool_asset_1": { + "type": "string" + }, + "pool_asset_2": { + "type": "string" + }, + "pool_id": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + } + }, + "ownership": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Ownership_for_String", + "description": "The contract's ownership info", + "type": "object", + "properties": { + "owner": { + "description": "The contract's current owner. `None` if the ownership has been renounced.", + "type": [ + "string", + "null" + ] + }, + "pending_expiry": { + "description": "The deadline for the pending owner to accept the ownership. `None` if there isn't a pending ownership transfer, or if a transfer exists and it doesn't have a deadline.", + "anyOf": [ + { + "$ref": "#/definitions/Expiration" + }, + { + "type": "null" + } + ] + }, + "pending_owner": { + "description": "The account who has been proposed to take over the ownership. `None` if there isn't a pending ownership transfer.", + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false, + "definitions": { + "Expiration": { + "description": "Expiration represents a point in time when some event happens. It can compare with a BlockInfo and will return is_expired() == true once the condition is hit (and for every block in the future)", + "oneOf": [ + { + "description": "AtHeight will expire when `env.block.height` >= height", + "type": "object", + "required": [ + "at_height" + ], + "properties": { + "at_height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + { + "description": "AtTime will expire when `env.block.time` >= time", + "type": "object", + "required": [ + "at_time" + ], + "properties": { + "at_time": { + "$ref": "#/definitions/Timestamp" + } + }, + "additionalProperties": false + }, + { + "description": "Never will never expire. Used to express the empty variant", + "type": "object", + "required": [ + "never" + ], + "properties": { + "never": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "Timestamp": { + "description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```", + "allOf": [ + { + "$ref": "#/definitions/Uint64" + } + ] + }, + "Uint64": { + "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```", + "type": "string" + } + } + } + } +} diff --git a/contracts/services/osmosis-gamm-lper/src/contract.rs b/contracts/services/osmosis-gamm-lper/src/contract.rs new file mode 100644 index 00000000..81883067 --- /dev/null +++ b/contracts/services/osmosis-gamm-lper/src/contract.rs @@ -0,0 +1,266 @@ +use std::str::FromStr; + +#[cfg(not(feature = "library"))] +use cosmwasm_std::entry_point; +use cosmwasm_std::{ + coin, coins, to_json_binary, BankMsg, Binary, Coin, CosmosMsg, Decimal, Deps, DepsMut, Env, + Fraction, MessageInfo, Response, StdError, StdResult, Uint128, +}; +use osmosis_std::{ + cosmwasm_to_proto_coins, + types::osmosis::{gamm::v1beta1::GammQuerier, poolmanager::v1beta1::PoolmanagerQuerier}, +}; +use valence_osmosis_utils::utils::{ + get_provide_liquidity_msg, get_provide_ss_liquidity_msg, DecimalRange, +}; +use valence_service_utils::{ + error::ServiceError, + execute_on_behalf_of, + msg::{ExecuteMsg, InstantiateMsg}, +}; + +use crate::msg::{ + ActionMsgs, Config, QueryMsg, ServiceConfig, ServiceConfigUpdate, ValenceLiquidPooler, +}; + +// version info for migration info +const CONTRACT_NAME: &str = env!("CARGO_PKG_NAME"); +const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn instantiate( + deps: DepsMut, + _env: Env, + _info: MessageInfo, + msg: InstantiateMsg, +) -> Result { + valence_service_base::instantiate(deps, CONTRACT_NAME, CONTRACT_VERSION, msg) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn execute( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: ExecuteMsg, +) -> Result { + valence_service_base::execute(deps, env, info, msg, process_action, update_config) +} + +pub fn update_config( + deps: DepsMut, + _env: Env, + _info: MessageInfo, + new_config: ServiceConfigUpdate, +) -> Result<(), ServiceError> { + new_config.update_config(deps) +} + +pub fn process_action( + deps: DepsMut, + _env: Env, + _info: MessageInfo, + msg: ActionMsgs, + cfg: Config, +) -> Result { + match msg { + ActionMsgs::ProvideDoubleSidedLiquidity { + expected_spot_price, + } => provide_double_sided_liquidity(deps, cfg, expected_spot_price), + ActionMsgs::ProvideSingleSidedLiquidity { + asset, + limit, + expected_spot_price, + } => provide_single_sided_liquidity(deps, cfg, asset, limit, expected_spot_price), + } +} + +fn provide_single_sided_liquidity( + deps: DepsMut, + cfg: Config, + asset: String, + limit: Uint128, + expected_spot_price: Option, +) -> Result { + // first we assert the input account balance + let input_acc_asset_bal = deps.querier.query_balance(&cfg.input_addr, &asset)?; + let pm_querier = PoolmanagerQuerier::new(&deps.querier); + let pool = pm_querier.query_pool_config(&cfg.lp_config)?; + let pool_ratio = pm_querier.query_spot_price(&cfg.lp_config)?; + + // assert the spot price to be within our expectations, + // if expectations are set. + if let Some(acceptable_spot_price_range) = expected_spot_price { + acceptable_spot_price_range.contains(pool_ratio)?; + } + + // if the input balance is greater than the limit, we provision the limit amount. + // otherwise we provision the full input balance. + let provision_amount = if input_acc_asset_bal.amount > limit { + limit + } else { + input_acc_asset_bal.amount + }; + + let share_out_amt = calculate_share_out_amt_swap( + &deps, + cfg.lp_config.pool_id, + coins(provision_amount.u128(), asset.to_string()), + )?; + + let liquidity_provision_msg = get_provide_ss_liquidity_msg( + cfg.input_addr.as_str(), + cfg.lp_config.pool_id, + coin(provision_amount.u128(), asset), + share_out_amt.to_string(), + )?; + + let transfer_lp_tokens_msg = BankMsg::Send { + to_address: cfg.output_addr.to_string(), + amount: coins( + share_out_amt.u128(), + pool.total_shares + .ok_or_else(|| StdError::generic_err("failed to get shares"))? + .denom, + ), + }; + + let delegated_input_acc_msgs = execute_on_behalf_of( + vec![liquidity_provision_msg, transfer_lp_tokens_msg.into()], + &cfg.input_addr.clone(), + )?; + + Ok(Response::default().add_message(delegated_input_acc_msgs)) +} + +pub fn provide_double_sided_liquidity( + deps: DepsMut, + cfg: Config, + expected_spot_price: Option, +) -> Result { + // first we assert the input account balances + let bal_asset_1 = deps + .querier + .query_balance(&cfg.input_addr, &cfg.lp_config.pool_asset_1)?; + let bal_asset_2 = deps + .querier + .query_balance(&cfg.input_addr, &cfg.lp_config.pool_asset_2)?; + + let pm_querier = PoolmanagerQuerier::new(&deps.querier); + + let pool_ratio = pm_querier.query_spot_price(&cfg.lp_config)?; + let pool = pm_querier.query_pool_config(&cfg.lp_config)?; + + // assert the spot price to be within our expectations, + // if expectations are set. + if let Some(acceptable_spot_price_range) = expected_spot_price { + acceptable_spot_price_range.contains(pool_ratio)?; + } + + let provision_coins = calculate_provision_amounts(bal_asset_1, bal_asset_2, pool_ratio)?; + + let share_out_amt = + calculate_share_out_amt_no_swap(&deps, cfg.lp_config.pool_id, provision_coins.clone())?; + + let liquidity_provision_msg: CosmosMsg = get_provide_liquidity_msg( + cfg.input_addr.as_str(), + cfg.lp_config.pool_id, + provision_coins, + share_out_amt.to_string(), + )?; + + let transfer_lp_tokens_msg = BankMsg::Send { + to_address: cfg.output_addr.to_string(), + amount: coins( + share_out_amt.u128(), + pool.total_shares + .ok_or_else(|| StdError::generic_err("failed to get shares"))? + .denom, + ), + }; + + let delegated_msgs = execute_on_behalf_of( + vec![liquidity_provision_msg, transfer_lp_tokens_msg.into()], + &cfg.input_addr.clone(), + )?; + + Ok(Response::default().add_message(delegated_msgs)) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { + match msg { + QueryMsg::Ownership {} => { + to_json_binary(&valence_service_base::get_ownership(deps.storage)?) + } + QueryMsg::GetProcessor {} => { + to_json_binary(&valence_service_base::get_processor(deps.storage)?) + } + QueryMsg::GetServiceConfig {} => { + let config: Config = valence_service_base::load_config(deps.storage)?; + to_json_binary(&config) + } + QueryMsg::GetRawServiceConfig {} => { + let raw_config: ServiceConfig = + valence_service_utils::raw_config::query_raw_service_config(deps.storage)?; + to_json_binary(&raw_config) + } + } +} + +pub fn calculate_share_out_amt_no_swap( + deps: &DepsMut, + pool_id: u64, + coins_in: Vec, +) -> StdResult { + let gamm_querier = GammQuerier::new(&deps.querier); + let shares_out = gamm_querier + .calc_join_pool_no_swap_shares(pool_id, cosmwasm_to_proto_coins(coins_in))? + .shares_out; + + let shares_u128 = Uint128::from_str(&shares_out)?; + + Ok(shares_u128) +} + +pub fn calculate_share_out_amt_swap( + deps: &DepsMut, + pool_id: u64, + coin_in: Vec, +) -> StdResult { + let gamm_querier = GammQuerier::new(&deps.querier); + let shares_out = gamm_querier + .calc_join_pool_shares(pool_id, cosmwasm_to_proto_coins(coin_in))? + .share_out_amount; + + let shares_u128 = Uint128::from_str(&shares_out)?; + + Ok(shares_u128) +} + +pub fn calculate_provision_amounts( + mut asset_1_bal: Coin, + mut asset_2_bal: Coin, + pool_ratio: Decimal, +) -> StdResult> { + // first we assume that we are going to provide all of asset_1 and up to all of asset_2 + // then we get the expected amount of asset_2 tokens to provide + let expected_asset_2_provision_amt = asset_1_bal + .amount + .checked_multiply_ratio(pool_ratio.numerator(), pool_ratio.denominator()) + .map_err(|e| StdError::generic_err(e.to_string()))?; + + // then we check if the expected amount of asset_2 tokens is greater than the available balance + if expected_asset_2_provision_amt > asset_2_bal.amount { + // if it is, we calculate the amount of asset_1 tokens to provide + asset_1_bal.amount = asset_2_bal + .amount + .checked_multiply_ratio(pool_ratio.denominator(), pool_ratio.numerator()) + .map_err(|e| StdError::generic_err(e.to_string()))?; + } else { + // if it is not, we provide all of asset_1 and the expected amount of asset_2 + asset_2_bal.amount = expected_asset_2_provision_amt; + } + + Ok(vec![asset_1_bal, asset_2_bal]) +} diff --git a/contracts/services/osmosis-gamm-lper/src/lib.rs b/contracts/services/osmosis-gamm-lper/src/lib.rs new file mode 100644 index 00000000..88bb55e5 --- /dev/null +++ b/contracts/services/osmosis-gamm-lper/src/lib.rs @@ -0,0 +1,4 @@ +pub mod contract; +pub mod msg; +#[cfg(test)] +mod testing; diff --git a/contracts/services/osmosis-gamm-lper/src/msg.rs b/contracts/services/osmosis-gamm-lper/src/msg.rs new file mode 100644 index 00000000..4c3ddfd0 --- /dev/null +++ b/contracts/services/osmosis-gamm-lper/src/msg.rs @@ -0,0 +1,166 @@ +use std::str::FromStr; + +use cosmwasm_schema::{cw_serde, QueryResponses}; +use cosmwasm_std::{ensure, Addr, Decimal, Deps, DepsMut, Empty, StdError, Uint128, Uint64}; +use cw_ownable::cw_ownable_query; +use osmosis_std::types::osmosis::{gamm::v1beta1::Pool, poolmanager::v1beta1::PoolmanagerQuerier}; +use valence_macros::{valence_service_query, ValenceServiceInterface}; +use valence_osmosis_utils::utils::DecimalRange; +use valence_service_utils::{ + error::ServiceError, msg::ServiceConfigValidation, ServiceAccountType, +}; + +#[cw_serde] +pub enum ActionMsgs { + ProvideDoubleSidedLiquidity { + expected_spot_price: Option, + }, + ProvideSingleSidedLiquidity { + expected_spot_price: Option, + asset: String, + limit: Uint128, + }, +} + +#[valence_service_query] +#[cw_ownable_query] +#[cw_serde] +#[derive(QueryResponses)] +pub enum QueryMsg {} + +#[cw_serde] +pub struct LiquidityProviderConfig { + pub pool_id: u64, + pub pool_asset_1: String, + pub pool_asset_2: String, +} + +pub trait ValenceLiquidPooler { + fn query_spot_price(&self, lp_config: &LiquidityProviderConfig) -> StdResult; + fn query_pool_config(&self, lp_config: &LiquidityProviderConfig) -> StdResult; +} + +impl ValenceLiquidPooler for PoolmanagerQuerier<'_, Empty> { + fn query_spot_price(&self, lp_config: &LiquidityProviderConfig) -> StdResult { + let spot_price_response = self.spot_price( + lp_config.pool_id, + lp_config.pool_asset_1.to_string(), + lp_config.pool_asset_2.to_string(), + )?; + + let pool_ratio = Decimal::from_str(&spot_price_response.spot_price)?; + + Ok(pool_ratio) + } + + fn query_pool_config(&self, lp_config: &LiquidityProviderConfig) -> StdResult { + let pool_response = self.pool(lp_config.pool_id)?; + let pool: Pool = pool_response + .pool + .ok_or_else(|| StdError::generic_err("failed to get pool"))? + .try_into() + .map_err(|_| StdError::generic_err("failed to decode proto"))?; + + Ok(pool) + } +} + +#[cw_serde] +#[derive(ValenceServiceInterface)] +pub struct ServiceConfig { + pub input_addr: ServiceAccountType, + pub output_addr: ServiceAccountType, + pub lp_config: LiquidityProviderConfig, +} + +impl ServiceConfig { + pub fn new( + input_addr: impl Into, + output_addr: impl Into, + lp_config: LiquidityProviderConfig, + ) -> Self { + ServiceConfig { + input_addr: input_addr.into(), + output_addr: output_addr.into(), + lp_config, + } + } + + fn do_validate( + &self, + api: &dyn cosmwasm_std::Api, + ) -> Result<(Addr, Addr, Uint64), ServiceError> { + let input_addr = self.input_addr.to_addr(api)?; + let output_addr = self.output_addr.to_addr(api)?; + + Ok((input_addr, output_addr, self.lp_config.pool_id.into())) + } +} + +#[cw_serde] +/// Validated service configuration +pub struct Config { + pub input_addr: Addr, + pub output_addr: Addr, + pub lp_config: LiquidityProviderConfig, +} + +impl ServiceConfigValidation for ServiceConfig { + #[cfg(not(target_arch = "wasm32"))] + fn pre_validate(&self, api: &dyn cosmwasm_std::Api) -> Result<(), ServiceError> { + self.do_validate(api)?; + Ok(()) + } + + fn validate(&self, deps: Deps) -> Result { + let (input_addr, output_addr, _pool_id) = self.do_validate(deps.api)?; + + let pm_querier = PoolmanagerQuerier::new(&deps.querier); + let pool = pm_querier.query_pool_config(&self.lp_config)?; + + // perform soft pool validation by asserting that the lp config assets + // are all present in the pool + let (mut asset_1_found, mut asset_2_found) = (false, false); + for pool_asset in pool.pool_assets { + if let Some(asset) = pool_asset.token { + if self.lp_config.pool_asset_1 == asset.denom { + asset_1_found = true; + } + if self.lp_config.pool_asset_2 == asset.denom { + asset_2_found = true; + } + } + } + + ensure!( + asset_1_found && asset_2_found, + ServiceError::ExecutionError("Pool does not contain expected assets".to_string()) + ); + + Ok(Config { + input_addr, + output_addr, + lp_config: self.lp_config.clone(), + }) + } +} + +impl ServiceConfigUpdate { + pub fn update_config(self, deps: DepsMut) -> Result<(), ServiceError> { + let mut config: Config = valence_service_base::load_config(deps.storage)?; + + if let Some(input_addr) = self.input_addr { + config.input_addr = input_addr.to_addr(deps.api)?; + } + + if let Some(output_addr) = self.output_addr { + config.output_addr = output_addr.to_addr(deps.api)?; + } + + if let Some(cfg) = self.lp_config { + config.lp_config = cfg; + } + + Ok(()) + } +} diff --git a/contracts/services/osmosis-gamm-lper/src/testing/mod.rs b/contracts/services/osmosis-gamm-lper/src/testing/mod.rs new file mode 100644 index 00000000..85b27a0c --- /dev/null +++ b/contracts/services/osmosis-gamm-lper/src/testing/mod.rs @@ -0,0 +1,4 @@ +#[cfg(test)] +mod test_suite; +#[cfg(test)] +mod tests; diff --git a/contracts/services/osmosis-gamm-lper/src/testing/test_suite.rs b/contracts/services/osmosis-gamm-lper/src/testing/test_suite.rs new file mode 100644 index 00000000..79b104a4 --- /dev/null +++ b/contracts/services/osmosis-gamm-lper/src/testing/test_suite.rs @@ -0,0 +1,197 @@ +use cosmwasm_std::{coin, Coin, Uint128}; + +use osmosis_test_tube::{ + osmosis_std::{ + try_proto_to_cosmwasm_coins, + types::{ + cosmos::bank::v1beta1::{MsgSend, QueryAllBalancesRequest}, + cosmwasm::wasm::v1::MsgExecuteContractResponse, + }, + }, + Account, Bank, ExecuteResponse, Module, Wasm, +}; +use valence_osmosis_utils::{ + suite::{ + approve_service, instantiate_input_account, OsmosisTestAppBuilder, OsmosisTestAppSetup, + OSMO_DENOM, TEST_DENOM, + }, + utils::DecimalRange, +}; +use valence_service_utils::msg::{ExecuteMsg, InstantiateMsg}; + +use crate::msg::{ActionMsgs, LiquidityProviderConfig, ServiceConfig, ServiceConfigUpdate}; + +const CONTRACT_PATH: &str = "../../../artifacts"; + +pub struct LPerTestSuite { + pub inner: OsmosisTestAppSetup, + pub lper_addr: String, + pub input_acc: String, + pub output_acc: String, +} + +impl Default for LPerTestSuite { + fn default() -> Self { + Self::new( + vec![ + coin(1_000_000u128, OSMO_DENOM), + coin(1_000_000u128, TEST_DENOM), + ], + None, + ) + } +} + +impl LPerTestSuite { + pub fn new(with_input_bals: Vec, lp_config: Option) -> Self { + let inner = OsmosisTestAppBuilder::new().build().unwrap(); + + // Create two base accounts + let wasm = Wasm::new(&inner.app); + + let wasm_byte_code = + std::fs::read(format!("{}/{}", CONTRACT_PATH, "valence_base_account.wasm")).unwrap(); + + let code_id = wasm + .store_code(&wasm_byte_code, None, inner.owner_acc()) + .unwrap() + .data + .code_id; + + let input_acc = instantiate_input_account(code_id, &inner); + let output_acc = instantiate_input_account(code_id, &inner); + let lper_addr = + instantiate_lper_contract(&inner, input_acc.clone(), output_acc.clone(), lp_config); + + // Approve the service for the input account + approve_service(&inner, input_acc.clone(), lper_addr.clone()); + + // Give some tokens to the input account so that it can provide liquidity + let bank = Bank::new(&inner.app); + + for input_bal in with_input_bals { + bank.send( + MsgSend { + from_address: inner.owner_acc().address(), + to_address: input_acc.clone(), + amount: vec![ + osmosis_test_tube::osmosis_std::types::cosmos::base::v1beta1::Coin { + denom: input_bal.denom.clone(), + amount: input_bal.amount.to_string(), + }, + ], + }, + inner.owner_acc(), + ) + .unwrap(); + } + + LPerTestSuite { + inner, + lper_addr, + input_acc, + output_acc, + } + } + + pub fn query_all_balances( + &self, + addr: &str, + ) -> cosmwasm_std_old::StdResult> { + let bank = Bank::new(&self.inner.app); + let resp = bank + .query_all_balances(&QueryAllBalancesRequest { + address: addr.to_string(), + pagination: None, + }) + .unwrap(); + let bals = try_proto_to_cosmwasm_coins(resp.balances)?; + Ok(bals) + } + + pub fn provide_two_sided_liquidity( + &self, + expected_spot_price: Option, + ) -> ExecuteResponse { + let wasm = Wasm::new(&self.inner.app); + + wasm.execute::>( + &self.lper_addr, + &ExecuteMsg::ProcessAction(ActionMsgs::ProvideDoubleSidedLiquidity { + expected_spot_price, + }), + &[], + self.inner.processor_acc(), + ) + .unwrap() + } + + pub fn provide_single_sided_liquidity( + &self, + asset: &str, + limit: Uint128, + expected_spot_price: Option, + ) -> ExecuteResponse { + let wasm = Wasm::new(&self.inner.app); + + wasm.execute::>( + &self.lper_addr, + &ExecuteMsg::ProcessAction(ActionMsgs::ProvideSingleSidedLiquidity { + expected_spot_price, + asset: asset.to_string(), + limit, + }), + &[], + self.inner.processor_acc(), + ) + .unwrap() + } +} + +fn instantiate_lper_contract( + setup: &OsmosisTestAppSetup, + input_acc: String, + output_acc: String, + lp_config: Option, +) -> String { + let wasm = Wasm::new(&setup.app); + let wasm_byte_code = std::fs::read(format!( + "{}/{}", + CONTRACT_PATH, "valence_osmosis_gamm_lper.wasm" + )) + .unwrap(); + + let code_id = wasm + .store_code(&wasm_byte_code, None, setup.owner_acc()) + .unwrap() + .data + .code_id; + + let pool_id = setup.balancer_pool_cfg.pool_id.u64(); + + let instantiate_msg = InstantiateMsg { + owner: setup.owner_acc().address(), + processor: setup.processor_acc().address(), + config: ServiceConfig::new( + input_acc.as_str(), + output_acc.as_str(), + lp_config.unwrap_or(LiquidityProviderConfig { + pool_id, + pool_asset_1: setup.balancer_pool_cfg.pool_asset1.to_string(), + pool_asset_2: setup.balancer_pool_cfg.pool_asset2.to_string(), + }), + ), + }; + + wasm.instantiate( + code_id, + &instantiate_msg, + None, + Some("lper"), + &[], + setup.owner_acc(), + ) + .unwrap() + .data + .address +} diff --git a/contracts/services/osmosis-gamm-lper/src/testing/tests.rs b/contracts/services/osmosis-gamm-lper/src/testing/tests.rs new file mode 100644 index 00000000..6b1c4a18 --- /dev/null +++ b/contracts/services/osmosis-gamm-lper/src/testing/tests.rs @@ -0,0 +1,129 @@ +use std::str::FromStr; + +use cosmwasm_std::{coin, Decimal}; +use valence_osmosis_utils::{suite::OSMO_DENOM, utils::DecimalRange}; + +use crate::msg::LiquidityProviderConfig; + +use super::test_suite::LPerTestSuite; + +#[test] +#[should_panic(expected = "Pool does not contain expected assets")] +fn test_provide_liquidity_fails_validation() { + LPerTestSuite::new( + vec![coin(1_000_000u128, OSMO_DENOM)], + Some(LiquidityProviderConfig { + pool_id: 1, + pool_asset_1: OSMO_DENOM.to_string(), + pool_asset_2: "random_denom".to_string(), + }), + ); +} + +#[test] +#[should_panic(expected = "Value is not within the expected range")] +fn test_provide_two_sided_liquidity_out_of_range() { + let setup = LPerTestSuite::default(); + + setup.provide_two_sided_liquidity(Some(DecimalRange::from(( + Decimal::from_str("0.0009").unwrap(), + Decimal::from_str("0.1111").unwrap(), + )))); +} + +#[test] +fn test_provide_two_sided_liquidity_no_range() { + let setup = LPerTestSuite::default(); + + let input_bals = setup.query_all_balances(&setup.input_acc).unwrap(); + let output_bals = setup.query_all_balances(&setup.output_acc).unwrap(); + + assert_eq!(input_bals.len(), 2); + assert_eq!(output_bals.len(), 0); + + setup.provide_two_sided_liquidity(None); + + let input_bals = setup.query_all_balances(&setup.input_acc).unwrap(); + let output_bals = setup.query_all_balances(&setup.output_acc).unwrap(); + assert_eq!(input_bals.len(), 0); + assert_eq!(output_bals.len(), 1); +} + +#[test] +fn test_provide_two_sided_liquidity_valid_range() { + let setup = LPerTestSuite::default(); + + let input_bals = setup.query_all_balances(&setup.input_acc).unwrap(); + let output_bals = setup.query_all_balances(&setup.output_acc).unwrap(); + + assert_eq!(input_bals.len(), 2); + assert_eq!(output_bals.len(), 0); + + setup.provide_two_sided_liquidity(Some(DecimalRange::from(( + Decimal::from_str("0.9").unwrap(), + Decimal::from_str("1.1").unwrap(), + )))); + + let input_bals = setup.query_all_balances(&setup.input_acc).unwrap(); + let output_bals = setup.query_all_balances(&setup.output_acc).unwrap(); + assert_eq!(input_bals.len(), 0); + assert_eq!(output_bals.len(), 1); +} + +#[test] +#[should_panic(expected = "Value is not within the expected range")] +fn test_provide_single_sided_liquidity_out_of_range() { + let setup = LPerTestSuite::default(); + + setup.provide_single_sided_liquidity( + OSMO_DENOM, + 10_000u128.into(), + Some(DecimalRange::from(( + Decimal::from_str("0.00001").unwrap(), + Decimal::from_str("0.11111").unwrap(), + ))), + ); +} + +#[test] +fn test_provide_single_sided_liquidity_no_range() { + let setup = LPerTestSuite::new(vec![coin(1_000_000u128, OSMO_DENOM)], None); + + let input_bals = setup.query_all_balances(&setup.input_acc).unwrap(); + let output_bals = setup.query_all_balances(&setup.output_acc).unwrap(); + + assert_eq!(input_bals.len(), 1); + assert_eq!(output_bals.len(), 0); + + setup.provide_single_sided_liquidity(OSMO_DENOM, 10_000u128.into(), None); + + let input_bals = setup.query_all_balances(&setup.input_acc).unwrap(); + let output_bals = setup.query_all_balances(&setup.output_acc).unwrap(); + assert_eq!(input_bals.len(), 1); + assert_eq!(output_bals.len(), 1); +} + +#[test] +fn test_provide_single_sided_liquidity_valid_range() { + let setup = LPerTestSuite::new(vec![coin(1_000_000u128, OSMO_DENOM)], None); + + let input_bals = setup.query_all_balances(&setup.input_acc).unwrap(); + let output_bals = setup.query_all_balances(&setup.output_acc).unwrap(); + + assert_eq!(input_bals.len(), 1); + assert_eq!(output_bals.len(), 0); + + setup.provide_single_sided_liquidity( + OSMO_DENOM, + 10_000u128.into(), + Some(DecimalRange::from(( + Decimal::from_str("0.9").unwrap(), + Decimal::from_str("1.1").unwrap(), + ))), + ); + + let input_bals = setup.query_all_balances(&setup.input_acc).unwrap(); + let output_bals = setup.query_all_balances(&setup.output_acc).unwrap(); + assert_eq!(input_bals.len(), 1); + assert_eq!(output_bals.len(), 1); +} diff --git a/devtools/optimize.sh b/devtools/optimize.sh index e246973e..a14a3c97 100755 --- a/devtools/optimize.sh +++ b/devtools/optimize.sh @@ -1,6 +1,5 @@ #!/bin/bash -cd .. if [[ $(uname -m) =~ "arm64" ]]; then \ docker run --rm -v "$(pwd)":/code \ --mount type=volume,source="$(basename "$(pwd)")_cache",target=/target \ diff --git a/local-interchaintest/examples/polytone.rs b/local-interchaintest/examples/polytone.rs index 599ca69d..88264e46 100644 --- a/local-interchaintest/examples/polytone.rs +++ b/local-interchaintest/examples/polytone.rs @@ -354,7 +354,7 @@ fn main() -> Result<(), Box> { .with_label("processor") .with_code_id(processor_code_id_on_juno) .with_salt_hex_encoded(&salt) - .with_msg(serde_json::to_value(&processor_instantiate_msg).unwrap()) + .with_msg(serde_json::to_value(processor_instantiate_msg).unwrap()) .with_flags(GAS_FLAGS) .send() .unwrap(); diff --git a/local-interchaintest/src/utils/authorization.rs b/local-interchaintest/src/utils/authorization.rs index 658883ff..dd93ea00 100644 --- a/local-interchaintest/src/utils/authorization.rs +++ b/local-interchaintest/src/utils/authorization.rs @@ -88,7 +88,7 @@ pub fn set_up_authorization_and_processor( .with_label("authorization") .with_code_id(authorization_code_id) .with_salt_hex_encoded(&salt) - .with_msg(serde_json::to_value(&authorization_instantiate_msg).unwrap()) + .with_msg(serde_json::to_value(authorization_instantiate_msg).unwrap()) .send() .unwrap(); diff --git a/local-interchaintest/src/utils/manager.rs b/local-interchaintest/src/utils/manager.rs index ea597f98..da1adf2b 100644 --- a/local-interchaintest/src/utils/manager.rs +++ b/local-interchaintest/src/utils/manager.rs @@ -27,7 +27,7 @@ pub fn setup_manager(test_ctx: &mut TestContext) -> Result<(), Box> { let artifacts_dir = format!("{}/artifacts", env::current_dir()?.to_str().unwrap()); let chain_infos = get_data_from_log(); let mut gc = get_global_config(); - gc.chains = chain_infos.clone(); + gc.chains.clone_from(&chain_infos); let mut uploader = test_ctx.build_tx_upload_contracts(); uploader.with_chain_name(NEUTRON_CHAIN_NAME); diff --git a/packages/osmosis-utils/Cargo.toml b/packages/osmosis-utils/Cargo.toml new file mode 100644 index 00000000..3756afe8 --- /dev/null +++ b/packages/osmosis-utils/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "valence-osmosis-utils" +version = { workspace = true } +edition = { workspace = true } +authors = ["Timewave Labs"] +description = "Utils for osmosis services" + +[features] +default = [] +testing = [ + "dep:osmosis-test-tube", +] + +[dependencies] +cosmwasm-std = { workspace = true } +cosmwasm-schema = { workspace = true } +osmosis-test-tube = { workspace = true, optional = true } +cosmwasm-std-polytone = { package = "cosmwasm-std", version = "1.5.7" } +valence-account-utils = { workspace = true } +osmosis-std = { workspace = true } +valence-service-utils = { workspace = true } diff --git a/packages/osmosis-utils/src/lib.rs b/packages/osmosis-utils/src/lib.rs new file mode 100644 index 00000000..c716bb02 --- /dev/null +++ b/packages/osmosis-utils/src/lib.rs @@ -0,0 +1,4 @@ +#[cfg(feature = "testing")] +pub mod suite; + +pub mod utils; diff --git a/packages/osmosis-utils/src/suite.rs b/packages/osmosis-utils/src/suite.rs new file mode 100644 index 00000000..c3057ec5 --- /dev/null +++ b/packages/osmosis-utils/src/suite.rs @@ -0,0 +1,132 @@ +use cosmwasm_std::{StdResult, Uint64}; + +use osmosis_test_tube::{Account, Gamm, Module, OsmosisTestApp, SigningAccount, Wasm}; + +pub const OSMO_DENOM: &str = "uosmo"; +pub const TEST_DENOM: &str = "utest"; + +pub struct OsmosisTestAppSetup { + pub app: OsmosisTestApp, + pub accounts: Vec, + pub balancer_pool_cfg: BalancerPool, +} + +pub struct BalancerPool { + pub pool_id: Uint64, + pub pool_liquidity_token: String, + pub pool_asset1: String, + pub pool_asset2: String, +} + +impl OsmosisTestAppSetup { + pub fn owner_acc(&self) -> &SigningAccount { + &self.accounts[0] + } + + pub fn processor_acc(&self) -> &SigningAccount { + &self.accounts[1] + } +} + +pub struct OsmosisTestAppBuilder { + fee_denom: String, + initial_balance: u128, + num_accounts: u64, +} + +impl Default for OsmosisTestAppBuilder { + fn default() -> Self { + Self::new() + } +} + +impl OsmosisTestAppBuilder { + pub fn new() -> Self { + Self { + fee_denom: OSMO_DENOM.to_string(), + initial_balance: 100_000_000_000_000_000, + num_accounts: 2, + } + } + + pub fn build(self) -> Result { + let app = OsmosisTestApp::new(); + + let accounts = app + .init_accounts( + &[ + cosmwasm_std_polytone::Coin::new(self.initial_balance, self.fee_denom.as_str()), + cosmwasm_std_polytone::Coin::new(self.initial_balance, TEST_DENOM), + ], + self.num_accounts, + ) + .map_err(|_| "Failed to initialize accounts")?; + + let balancer_pool = setup_balancer_pool(&app, &accounts[0]).unwrap(); + + Ok(OsmosisTestAppSetup { + app, + accounts, + balancer_pool_cfg: balancer_pool, + }) + } +} + +fn setup_balancer_pool(app: &OsmosisTestApp, creator: &SigningAccount) -> StdResult { + let gamm = Gamm::new(app); + + // create balancer pool with basic configuration + let pool_liquidity = vec![ + cosmwasm_std_polytone::Coin::new(100_000u128, OSMO_DENOM), + cosmwasm_std_polytone::Coin::new(100_000u128, TEST_DENOM), + ]; + let pool_id = gamm + .create_basic_pool(&pool_liquidity, creator) + .unwrap() + .data + .pool_id; + + let pool = gamm.query_pool(pool_id).unwrap(); + + let pool_liquidity_token = pool.total_shares.unwrap().denom; + + let balancer_pool = BalancerPool { + pool_id: pool_id.into(), + pool_liquidity_token, + pool_asset1: OSMO_DENOM.to_string(), + pool_asset2: TEST_DENOM.to_string(), + }; + + Ok(balancer_pool) +} + +pub fn approve_service(setup: &OsmosisTestAppSetup, account_addr: String, service_addr: String) { + let wasm = Wasm::new(&setup.app); + wasm.execute::( + &account_addr, + &valence_account_utils::msg::ExecuteMsg::ApproveService { + service: service_addr, + }, + &[], + setup.owner_acc(), + ) + .unwrap(); +} + +pub fn instantiate_input_account(code_id: u64, setup: &OsmosisTestAppSetup) -> String { + let wasm = Wasm::new(&setup.app); + wasm.instantiate( + code_id, + &valence_account_utils::msg::InstantiateMsg { + admin: setup.owner_acc().address(), + approved_services: vec![], + }, + None, + Some("base_account"), + &[], + setup.owner_acc(), + ) + .unwrap() + .data + .address +} diff --git a/packages/osmosis-utils/src/utils.rs b/packages/osmosis-utils/src/utils.rs new file mode 100644 index 00000000..d0f0594d --- /dev/null +++ b/packages/osmosis-utils/src/utils.rs @@ -0,0 +1,67 @@ +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{ensure, Coin, CosmosMsg, Decimal, StdResult}; +use osmosis_std::{ + cosmwasm_to_proto_coins, + types::osmosis::gamm::v1beta1::{MsgJoinPool, MsgJoinSwapExternAmountIn}, +}; +use valence_service_utils::error::ServiceError; + +#[cw_serde] +pub struct DecimalRange { + min: Decimal, + max: Decimal, +} + +impl From<(Decimal, Decimal)> for DecimalRange { + fn from((min, max): (Decimal, Decimal)) -> Self { + DecimalRange { min, max } + } +} + +impl DecimalRange { + pub fn contains(&self, value: Decimal) -> Result<(), ServiceError> { + ensure!( + value >= self.min && value <= self.max, + ServiceError::ExecutionError("Value is not within the expected range".to_string()) + ); + Ok(()) + } +} + +pub fn get_provide_liquidity_msg( + input_addr: &str, + pool_id: u64, + provision_coins: Vec, + share_out_amt: String, +) -> StdResult { + let tokens_in_proto = cosmwasm_to_proto_coins(provision_coins); + + let msg_join_pool_no_swap: CosmosMsg = MsgJoinPool { + sender: input_addr.to_string(), + pool_id, + share_out_amount: share_out_amt, + token_in_maxs: tokens_in_proto, + } + .into(); + + Ok(msg_join_pool_no_swap) +} + +pub fn get_provide_ss_liquidity_msg( + input_addr: &str, + pool_id: u64, + provision_coin: Coin, + share_out_amt: String, +) -> StdResult { + let proto_coin_in = cosmwasm_to_proto_coins(vec![provision_coin]); + + let msg_join_pool_yes_swap: CosmosMsg = MsgJoinSwapExternAmountIn { + sender: input_addr.to_string(), + pool_id, + token_in: Some(proto_coin_in[0].clone()), + share_out_min_amount: share_out_amt, + } + .into(); + + Ok(msg_join_pool_yes_swap) +} diff --git a/workflow-manager/src/domain/mod.rs b/workflow-manager/src/domain/mod.rs index 1ef78369..0fe11377 100644 --- a/workflow-manager/src/domain/mod.rs +++ b/workflow-manager/src/domain/mod.rs @@ -54,7 +54,7 @@ impl fmt::Display for Domain { impl Domain { pub fn from_string(input: String) -> Result { - let mut split = input.split(":"); + let mut split = input.split(':'); let domain = split.next().context("Domain is missing")?;