diff --git a/.github/workflows/Basic.yml b/.github/workflows/Basic.yml index 3890a07..e371d6d 100644 --- a/.github/workflows/Basic.yml +++ b/.github/workflows/Basic.yml @@ -17,7 +17,7 @@ jobs: uses: actions-rs/toolchain@v1 with: profile: minimal - toolchain: 1.60.0 + toolchain: stable target: wasm32-unknown-unknown override: true @@ -48,7 +48,7 @@ jobs: uses: actions-rs/toolchain@v1 with: profile: minimal - toolchain: 1.60.0 + toolchain: stable override: true components: rustfmt, clippy @@ -62,14 +62,4 @@ jobs: uses: actions-rs/cargo@v1 with: command: clippy - args: -- -D warnings - - - name: Generate Schema - uses: actions-rs/cargo@v1 - with: - command: schema - args: --locked - - - name: Schema Changes - # fails if any changes not committed - run: git diff --exit-code schema + args: -- -D warnings \ No newline at end of file diff --git a/.github/workflows/Release.yml b/.github/workflows/Release.yml index a9d5979..99a0a63 100644 --- a/.github/workflows/Release.yml +++ b/.github/workflows/Release.yml @@ -15,11 +15,7 @@ jobs: with: command: install args: cargo-run-script - - name: Run cargo optimize - uses: actions-rs/cargo@v1 - with: - command: run-script - args: optimize + - run: ./scripts/build_release.sh - name: Get release ID id: get_release uses: bruceadams/get-release@v1.2.3 diff --git a/Cargo.lock b/Cargo.lock index a3fe2e9..71f1e1e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,17 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "ahash" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" +dependencies = [ + "getrandom", + "once_cell", + "version_check", +] + [[package]] name = "ahash" version = "0.8.11" @@ -22,9 +33,9 @@ checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" [[package]] name = "anyhow" -version = "1.0.89" +version = "1.0.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86fdf8605db99b54d3cd748a44c6d04df638eb5dafb219b135d0149bd0db01f6" +checksum = "c042108f3ed77fd83760a5fd79b53be043192bb3b9dba91d8c574c0ada7850c8" [[package]] name = "ark-bls12-381" @@ -67,7 +78,7 @@ dependencies = [ "ark-serialize", "ark-std", "derivative", - "digest", + "digest 0.10.7", "itertools 0.10.5", "num-bigint", "num-traits", @@ -121,7 +132,7 @@ checksum = "adb7b85a02b83d2f22f89bd5cac66c9c89474240cb6207cb1efc16d098e822a5" dependencies = [ "ark-serialize-derive", "ark-std", - "digest", + "digest 0.10.7", "num-bigint", ] @@ -143,7 +154,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94893f1e0c6eeab764ade8dc4c0db24caf4fe7cbbaafc0eba0a9030f447b5185" dependencies = [ "num-traits", - "rand", + "rand 0.8.5", "rayon", ] @@ -159,12 +170,37 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + [[package]] name = "base64" version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + +[[package]] +name = "basic-stableswap-sim" +version = "0.1.0" +dependencies = [ + "cosmwasm-std 1.5.8", +] + +[[package]] +name = "bech32" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d86b93f97252c47b41663388e6d155714a9d0c398b99f1005cbc5f978b29f445" + [[package]] name = "bech32" version = "0.11.0" @@ -186,12 +222,27 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "bitflags" version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +[[package]] +name = "block-buffer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +dependencies = [ + "generic-array", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -201,6 +252,12 @@ dependencies = [ "generic-array", ] +[[package]] +name = "bnum" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56953345e39537a3e18bdaeba4cb0c58a78c1f61f361dc0fa7c5c7340ae87c5f" + [[package]] name = "bnum" version = "0.11.0" @@ -215,9 +272,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.7.2" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "428d9aa8fbc0670b7b8d6030a7fadd0f86151cae55e4dbbece15f3780a3dfaf3" +checksum = "9ac0150caa2ae65ca5bd83f25c7de183dea78d4d366469f148435e2acfbad0da" [[package]] name = "cfg-if" @@ -225,6 +282,15 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "cloudabi" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f" +dependencies = [ + "bitflags 1.3.2", +] + [[package]] name = "const-oid" version = "0.9.6" @@ -237,6 +303,19 @@ version = "2.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f6ceb8624260d0d3a67c4e1a1d43fc7e9406720afbcb124521501dd138f90aa" +[[package]] +name = "cosmwasm-crypto" +version = "1.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58535cbcd599b3c193e3967c8292fe1dbbb5de7c2a2d87380661091dd4744044" +dependencies = [ + "digest 0.10.7", + "ed25519-zebra 3.1.0", + "k256", + "rand_core 0.6.4", + "thiserror", +] + [[package]] name = "cosmwasm-crypto" version = "2.1.4" @@ -248,18 +327,27 @@ dependencies = [ "ark-ff", "ark-serialize", "cosmwasm-core", - "digest", + "digest 0.10.7", "ecdsa", - "ed25519-zebra", + "ed25519-zebra 4.0.3", "k256", "num-traits", "p256", - "rand_core", + "rand_core 0.6.4", "rayon", - "sha2", + "sha2 0.10.8", "thiserror", ] +[[package]] +name = "cosmwasm-derive" +version = "1.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8e07de16c800ac82fd188d055ecdb923ead0cf33960d3350089260bb982c09f" +dependencies = [ + "syn 1.0.109", +] + [[package]] name = "cosmwasm-derive" version = "2.1.4" @@ -268,14 +356,14 @@ checksum = "1b5658b1dc64e10b56ae7a449f678f96932a96f6cfad1769d608d1d1d656480a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] name = "cosmwasm-schema" -version = "2.1.4" +version = "1.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f86b4d949b6041519c58993a73f4bbfba8083ba14f7001eae704865a09065845" +checksum = "93d388adfa9cb449557a92e9318121ac1a481fc4f599213b03a5b62699b403b4" dependencies = [ "cosmwasm-schema-derive", "schemars", @@ -286,13 +374,35 @@ dependencies = [ [[package]] name = "cosmwasm-schema-derive" -version = "2.1.4" +version = "1.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8ef1b5835a65fcca3ab8b9a02b4f4dacc78e233a5c2f20b270efb9db0666d12" +checksum = "2411b389e56e6484f81ba955b758d02522d620c98fc960c4bd2251d48b7aa19f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 1.0.109", +] + +[[package]] +name = "cosmwasm-std" +version = "1.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c21fde95ccd20044a23c0ac6fd8c941f3e8c158169dc94b5aa6491a2d9551a8d" +dependencies = [ + "base64 0.21.7", + "bech32 0.9.1", + "bnum 0.10.0", + "cosmwasm-crypto 1.5.8", + "cosmwasm-derive 1.5.8", + "derivative", + "forward_ref", + "hex", + "schemars", + "serde", + "serde-json-wasm 0.5.2", + "sha2 0.10.8", + "static_assertions 1.1.0", + "thiserror", ] [[package]] @@ -301,20 +411,20 @@ version = "2.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70eb7ab0c1e99dd6207496963ba2a457c4128ac9ad9c72a83f8d9808542b849b" dependencies = [ - "base64", - "bech32", - "bnum", + "base64 0.22.1", + "bech32 0.11.0", + "bnum 0.11.0", "cosmwasm-core", - "cosmwasm-crypto", - "cosmwasm-derive", + "cosmwasm-crypto 2.1.4", + "cosmwasm-derive 2.1.4", "derive_more", "hex", - "rand_core", + "rand_core 0.6.4", "schemars", "serde", - "serde-json-wasm", - "sha2", - "static_assertions", + "serde-json-wasm 1.0.1", + "sha2 0.10.8", + "static_assertions 1.1.0", "thiserror", ] @@ -365,7 +475,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" dependencies = [ "generic-array", - "rand_core", + "rand_core 0.6.4", "subtle", "zeroize", ] @@ -380,6 +490,19 @@ dependencies = [ "typenum", ] +[[package]] +name = "curve25519-dalek" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b9fdf9972b2bd6af2d913799d9ebc165ea4d2e65878e329d9c6b372c4491b61" +dependencies = [ + "byteorder", + "digest 0.9.0", + "rand_core 0.5.1", + "subtle", + "zeroize", +] + [[package]] name = "curve25519-dalek" version = "4.1.3" @@ -389,7 +512,7 @@ dependencies = [ "cfg-if", "cpufeatures", "curve25519-dalek-derive", - "digest", + "digest 0.10.7", "fiat-crypto", "rustc_version", "subtle", @@ -404,71 +527,139 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] name = "cw-address-like" -version = "2.0.0" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73553ee4dad5b1678977ff603e72c3fdd41518ca2b0bd9b245b21e4c72eafa9e" +checksum = "451a4691083a88a3c0630a8a88799e9d4cd6679b7ce8ff22b8da2873ff31d380" dependencies = [ - "cosmwasm-std", + "cosmwasm-std 1.5.8", ] [[package]] name = "cw-asset" -version = "4.0.0" +version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7eaa5ed63f1d767d92c7bada3b883aa6ec90ff09e3f5bf72ff4514dcbeb9c4f2" +checksum = "c999a12f8cd8736f6f86e9a4ede5905530cb23cfdef946b9da1c506ad1b70799" dependencies = [ "cosmwasm-schema", - "cosmwasm-std", + "cosmwasm-std 1.5.8", "cw-address-like", - "cw-storage-plus", - "cw20", + "cw-storage-plus 1.2.0", + "cw20 1.1.2", "thiserror", ] [[package]] name = "cw-multi-test" -version = "2.1.1" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0ae276e7a06ad1b7e7da78a3d68aba80634cde30ee7fe8259a94e653603fef8" +checksum = "91fc33b1d65c102d72f46548c64dca423c337e528d6747d0c595316aa65f887b" dependencies = [ "anyhow", - "bech32", - "cosmwasm-std", - "cw-storage-plus", - "cw-utils", + "bech32 0.11.0", + "cosmwasm-std 1.5.8", + "cw-storage-plus 1.2.0", + "cw-utils 1.0.3", "derivative", "itertools 0.13.0", - "prost", + "prost 0.12.6", "schemars", "serde", - "sha2", + "sha2 0.10.8", "thiserror", ] +[[package]] +name = "cw-storage-plus" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc6cf70ef7686e2da9ad7b067c5942cd3e88dd9453f7af42f54557f8af300fb0" +dependencies = [ + "cosmwasm-std 1.5.8", + "schemars", + "serde", +] + +[[package]] +name = "cw-storage-plus" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5ff29294ee99373e2cd5fd21786a3c0ced99a52fec2ca347d565489c61b723c" +dependencies = [ + "cosmwasm-std 1.5.8", + "schemars", + "serde", +] + [[package]] name = "cw-storage-plus" version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f13360e9007f51998d42b1bc6b7fa0141f74feae61ed5fd1e5b0a89eec7b5de1" dependencies = [ - "cosmwasm-std", + "cosmwasm-std 2.1.4", "schemars", "serde", ] [[package]] name = "cw-utils" -version = "2.0.0" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ae0b69fa7679de78825b4edeeec045066aa2b2c4b6e063d80042e565bb4da5c" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std 1.5.8", + "cw2 0.15.1", + "schemars", + "semver", + "serde", + "thiserror", +] + +[[package]] +name = "cw-utils" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c4a657e5caacc3a0d00ee96ca8618745d050b8f757c709babafb81208d4239c" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std 1.5.8", + "cw2 1.1.2", + "schemars", + "semver", + "serde", + "thiserror", +] + +[[package]] +name = "cw1" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1605722190afd93bfea6384b88224d1cfe50ebf70d2e10641535da79fa70e83" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std 1.5.8", + "schemars", + "serde", +] + +[[package]] +name = "cw1-whitelist" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07dfee7f12f802431a856984a32bce1cb7da1e6c006b5409e3981035ce562dec" +checksum = "81bb3e9dc87f4ff26547f4e27e0ba3c82034372f21b2f55527fb52b542637d8d" dependencies = [ "cosmwasm-schema", - "cosmwasm-std", + "cosmwasm-std 1.5.8", + "cw-storage-plus 1.2.0", + "cw-utils 1.0.3", + "cw1", + "cw2 1.1.2", "schemars", "serde", "thiserror", @@ -476,13 +667,26 @@ dependencies = [ [[package]] name = "cw2" -version = "2.0.0" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b04852cd38f044c0751259d5f78255d07590d136b8a86d4e09efdd7666bd6d27" +checksum = "5abb8ecea72e09afff830252963cb60faf945ce6cef2c20a43814516082653da" dependencies = [ "cosmwasm-schema", - "cosmwasm-std", - "cw-storage-plus", + "cosmwasm-std 1.5.8", + "cw-storage-plus 0.15.1", + "schemars", + "serde", +] + +[[package]] +name = "cw2" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6c120b24fbbf5c3bedebb97f2cc85fbfa1c3287e09223428e7e597b5293c1fa" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std 1.5.8", + "cw-storage-plus 1.2.0", "schemars", "semver", "serde", @@ -491,15 +695,60 @@ dependencies = [ [[package]] name = "cw20" -version = "2.0.0" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6025276fb6e603e974c21f3e4606982cdc646080e8fba3198816605505e1d9a" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std 1.5.8", + "cw-utils 0.15.1", + "schemars", + "serde", +] + +[[package]] +name = "cw20" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "526e39bb20534e25a1cd0386727f0038f4da294e5e535729ba3ef54055246abd" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std 1.5.8", + "cw-utils 1.0.3", + "schemars", + "serde", +] + +[[package]] +name = "cw20-base" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a42212b6bf29bbdda693743697c621894723f35d3db0d5df930be22903d0e27c" +checksum = "17ad79e86ea3707229bf78df94e08732e8f713207b4a77b2699755596725e7d9" dependencies = [ "cosmwasm-schema", - "cosmwasm-std", - "cw-utils", + "cosmwasm-std 1.5.8", + "cw-storage-plus 1.2.0", + "cw2 1.1.2", + "cw20 1.1.2", "schemars", + "semver", "serde", + "thiserror", +] + +[[package]] +name = "cw3" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2967fbd073d4b626dd9e7148e05a84a3bebd9794e71342e12351110ffbb12395" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std 1.5.8", + "cw-utils 1.0.3", + "cw20 1.1.2", + "schemars", + "serde", + "thiserror", ] [[package]] @@ -540,17 +789,26 @@ checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.85", "unicode-xid", ] +[[package]] +name = "digest" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array", +] + [[package]] name = "digest" version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "block-buffer", + "block-buffer 0.10.4", "const-oid", "crypto-common", "subtle", @@ -569,10 +827,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" dependencies = [ "der", - "digest", + "digest 0.10.7", "elliptic-curve", "rfc6979", "signature", + "spki", ] [[package]] @@ -584,18 +843,33 @@ dependencies = [ "signature", ] +[[package]] +name = "ed25519-zebra" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c24f403d068ad0b359e577a77f92392118be3f3c927538f2bb544a5ecd828c6" +dependencies = [ + "curve25519-dalek 3.2.0", + "hashbrown 0.12.3", + "hex", + "rand_core 0.6.4", + "serde", + "sha2 0.9.9", + "zeroize", +] + [[package]] name = "ed25519-zebra" version = "4.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d9ce6874da5d4415896cd45ffbc4d1cfc0c4f9c079427bd870742c30f2f65a9" dependencies = [ - "curve25519-dalek", + "curve25519-dalek 4.1.3", "ed25519", "hashbrown 0.14.5", "hex", - "rand_core", - "sha2", + "rand_core 0.6.4", + "sha2 0.10.8", "zeroize", ] @@ -613,11 +887,12 @@ checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" dependencies = [ "base16ct", "crypto-bigint", - "digest", + "digest 0.10.7", "ff", "generic-array", "group", - "rand_core", + "pkcs8", + "rand_core 0.6.4", "sec1", "subtle", "zeroize", @@ -633,6 +908,42 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "ethbloom" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3932e82d64d347a045208924002930dc105a138995ccdc1479d0f05f0359f17c" +dependencies = [ + "crunchy", + "fixed-hash 0.3.2", + "impl-rlp", + "impl-serde", + "tiny-keccak", +] + +[[package]] +name = "ethereum-types" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b054df51e53f253837ea422681215b42823c02824bde982699d0dceecf6165a1" +dependencies = [ + "crunchy", + "ethbloom", + "ethereum-types-serialize", + "fixed-hash 0.3.2", + "serde", + "uint 0.5.0", +] + +[[package]] +name = "ethereum-types-serialize" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1873d77b32bc1891a79dad925f2acbc318ee942b38b9110f9dbc5fbeffcea350" +dependencies = [ + "serde", +] + [[package]] name = "fastrand" version = "2.1.1" @@ -645,7 +956,7 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ded41244b729663b1e574f1b4fb731469f69f79c17667b5d776b16cda0479449" dependencies = [ - "rand_core", + "rand_core 0.6.4", "subtle", ] @@ -655,12 +966,46 @@ version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" +[[package]] +name = "fixed-hash" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1a683d1234507e4f3bf2736eeddf0de1dc65996dc0164d57eba0a74bcf29489" +dependencies = [ + "byteorder", + "heapsize", + "rand 0.5.6", + "rustc-hex", + "static_assertions 0.2.5", +] + +[[package]] +name = "fixed-hash" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "835c052cb0c08c1acf6ffd71c022172e18723949c8282f2b9f27efbc51e64534" +dependencies = [ + "static_assertions 1.1.0", +] + [[package]] name = "fnv" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "forward_ref" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8cbd1169bd7b4a0a20d92b9af7a7e0422888bd38a6f5ec29c1fd8c1558a272e" + +[[package]] +name = "fuchsia-cprng" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" + [[package]] name = "generic-array" version = "0.14.7" @@ -690,17 +1035,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" dependencies = [ "ff", - "rand_core", + "rand_core 0.6.4", "subtle", ] +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash 0.7.8", +] + [[package]] name = "hashbrown" version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" dependencies = [ - "ahash", + "ahash 0.8.11", ] [[package]] @@ -709,15 +1063,27 @@ version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" dependencies = [ - "ahash", + "ahash 0.8.11", "allocator-api2", ] +[[package]] +name = "heapsize" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1679e6ea370dee694f91f1dc469bf94cf8f52051d147aec3e1f9497c6fc22461" +dependencies = [ + "winapi", +] + [[package]] name = "hex" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +dependencies = [ + "serde", +] [[package]] name = "hmac" @@ -725,7 +1091,66 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" dependencies = [ - "digest", + "digest 0.10.7", +] + +[[package]] +name = "impl-rlp" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f7a72f11830b52333f36e3b09a288333888bf54380fd0ac0790a3c31ab0f3c5" +dependencies = [ + "rlp", +] + +[[package]] +name = "impl-serde" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58e3cae7e99c7ff5a995da2cf78dd0a5383740eda71d98cf7b1910c301ac69b8" +dependencies = [ + "serde", +] + +[[package]] +name = "injective-cosmwasm" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f04d173eced96ba05f33a0af9d2fce14d119335b4ae4c91d504018177b6fbfe" +dependencies = [ + "cosmwasm-std 2.1.4", + "cw-storage-plus 2.0.0", + "ethereum-types", + "hex", + "injective-math", + "schemars", + "serde", + "serde_repr", + "subtle-encoding", + "tiny-keccak", +] + +[[package]] +name = "injective-math" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db4e31ffb7dff274e0be1117bc8f1240f6572d6157be2c4daf13ff82eaaddd85" +dependencies = [ + "cosmwasm-std 1.5.8", + "ethereum-types", + "primitive-types", + "schemars", + "serde", + "subtle-encoding", +] + +[[package]] +name = "integer-sqrt" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "276ec31bcb4a9ee45f58bec6f9ec700ae4cf4f4f8f2fa7e06cb406bd5ffdd770" +dependencies = [ + "num-traits", ] [[package]] @@ -737,6 +1162,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + [[package]] name = "itertools" version = "0.13.0" @@ -761,7 +1195,9 @@ dependencies = [ "cfg-if", "ecdsa", "elliptic-curve", - "sha2", + "once_cell", + "sha2 0.10.8", + "signature", ] [[package]] @@ -772,9 +1208,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.159" +version = "0.2.161" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5" +checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1" [[package]] name = "libm" @@ -829,6 +1265,12 @@ version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + [[package]] name = "p256" version = "0.13.2" @@ -838,7 +1280,7 @@ dependencies = [ "ecdsa", "elliptic-curve", "primeorder", - "sha2", + "sha2 0.10.8", ] [[package]] @@ -846,19 +1288,85 @@ name = "palomadex" version = "0.1.0" dependencies = [ "cosmwasm-schema", - "cosmwasm-std", + "cosmwasm-std 1.5.8", "cw-asset", "cw-multi-test", - "cw-storage-plus", - "cw-utils", - "cw2", - "cw20", + "cw-storage-plus 1.2.0", + "cw-utils 1.0.3", + "cw20 1.1.2", "itertools 0.13.0", + "palomadex-circular-buffer", "schemars", "serde", "test-case", "thiserror", - "uint", + "uint 0.10.0", +] + +[[package]] +name = "palomadex-circular-buffer" +version = "0.1.0" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std 1.5.8", + "cw-storage-plus 1.2.0", + "thiserror", +] + +[[package]] +name = "palomadex-factory" +version = "0.1.0" +dependencies = [ + "anyhow", + "cosmwasm-schema", + "cosmwasm-std 1.5.8", + "cw-multi-test", + "cw-storage-plus 1.2.0", + "cw-utils 1.0.3", + "cw20 1.1.2", + "cw20-base", + "itertools 0.13.0", + "palomadex", + "palomadex-pair", + "prost 0.13.3", + "schemars", + "serde", + "thiserror", +] + +[[package]] +name = "palomadex-mocks" +version = "0.2.0" +dependencies = [ + "anyhow", + "cosmwasm-schema", + "cosmwasm-std 1.5.8", + "cw-multi-test", + "cw-utils 1.0.3", + "cw1-whitelist", + "cw20 0.15.1", + "cw20-base", + "cw3", + "injective-cosmwasm", + "palomadex", + "palomadex-factory", + "palomadex-native-coin-registry", + "palomadex-pair", + "palomadex-pair-stable", + "schemars", + "serde", +] + +[[package]] +name = "palomadex-native-coin-registry" +version = "0.1.0" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std 1.5.8", + "cw-multi-test", + "cw-storage-plus 1.2.0", + "palomadex", + "thiserror", ] [[package]] @@ -866,26 +1374,80 @@ name = "palomadex-pair" version = "0.1.0" dependencies = [ "cosmwasm-schema", - "cosmwasm-std", + "cosmwasm-std 1.5.8", "cw-multi-test", - "cw-storage-plus", - "cw-utils", - "cw2", - "cw20", + "cw-storage-plus 1.2.0", + "cw-utils 1.0.3", + "cw20 1.1.2", "palomadex", "proptest", - "prost", + "prost 0.13.3", "schemars", "serde", "thiserror", ] +[[package]] +name = "palomadex-pair-stable" +version = "0.1.0" +dependencies = [ + "anyhow", + "basic-stableswap-sim", + "cosmwasm-schema", + "cosmwasm-std 1.5.8", + "cw-multi-test", + "cw-storage-plus 1.2.0", + "cw-utils 1.0.3", + "cw20 1.1.2", + "cw20-base", + "derivative", + "itertools 0.13.0", + "palomadex", + "palomadex-circular-buffer", + "palomadex-factory", + "palomadex-mocks", + "palomadex-native-coin-registry", + "proptest", + "prost 0.13.3", + "schemars", + "serde", + "thiserror", +] + +[[package]] +name = "palomadex-router" +version = "0.1.0" +dependencies = [ + "anyhow", + "cosmwasm-schema", + "cosmwasm-std 1.5.8", + "cw-multi-test", + "cw-storage-plus 1.2.0", + "cw20 1.1.2", + "cw20-base", + "integer-sqrt", + "palomadex", + "palomadex-factory", + "palomadex-pair", + "thiserror", +] + [[package]] name = "paste" version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + [[package]] name = "ppv-lite86" version = "0.2.20" @@ -904,11 +1466,21 @@ dependencies = [ "elliptic-curve", ] +[[package]] +name = "primitive-types" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b34d9fd68ae0b74a41b21c03c2f62847aa0ffea044eee893b4c140b37e244e2" +dependencies = [ + "fixed-hash 0.8.0", + "uint 0.9.5", +] + [[package]] name = "proc-macro2" -version = "1.0.87" +version = "1.0.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3e4daa0dcf6feba26f985457cdf104d4b4256fc5a09547140f3631bb076b19a" +checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e" dependencies = [ "unicode-ident", ] @@ -921,10 +1493,10 @@ checksum = "b4c2511913b88df1637da85cc8d96ec8e43a3f8bb8ccb71ee1ac240d6f3df58d" dependencies = [ "bit-set", "bit-vec", - "bitflags", + "bitflags 2.6.0", "lazy_static", "num-traits", - "rand", + "rand 0.8.5", "rand_chacha", "rand_xorshift", "regex-syntax", @@ -933,6 +1505,16 @@ dependencies = [ "unarray", ] +[[package]] +name = "prost" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "deb1435c188b76130da55f17a466d252ff7b1418b2ad3e037d127b94e3411f29" +dependencies = [ + "bytes", + "prost-derive 0.12.6", +] + [[package]] name = "prost" version = "0.13.3" @@ -940,7 +1522,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b0487d90e047de87f984913713b85c601c05609aad5b0df4b4573fbf69aa13f" dependencies = [ "bytes", - "prost-derive", + "prost-derive 0.13.3", +] + +[[package]] +name = "prost-derive" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81bddcdb20abf9501610992b6759a4c888aef7d1a7247ef75e2404275ac24af1" +dependencies = [ + "anyhow", + "itertools 0.12.1", + "proc-macro2", + "quote", + "syn 2.0.85", ] [[package]] @@ -953,7 +1548,7 @@ dependencies = [ "itertools 0.13.0", "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] @@ -971,6 +1566,19 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rand" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c618c47cd3ebd209790115ab837de41425723956ad3ce2e6a7f09890947cacb9" +dependencies = [ + "cloudabi", + "fuchsia-cprng", + "libc", + "rand_core 0.3.1", + "winapi", +] + [[package]] name = "rand" version = "0.8.5" @@ -979,7 +1587,7 @@ checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", "rand_chacha", - "rand_core", + "rand_core 0.6.4", ] [[package]] @@ -989,9 +1597,30 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.6.4", ] +[[package]] +name = "rand_core" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" +dependencies = [ + "rand_core 0.4.2", +] + +[[package]] +name = "rand_core" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" + [[package]] name = "rand_core" version = "0.6.4" @@ -1007,7 +1636,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d25bf25ec5ae4a3f1b92f929810509a2f53d7dca2f50b794ff57e3face536c8f" dependencies = [ - "rand_core", + "rand_core 0.6.4", ] [[package]] @@ -1046,6 +1675,21 @@ dependencies = [ "subtle", ] +[[package]] +name = "rlp" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1190dcc8c3a512f1eef5d09bb8c84c7f39e1054e174d1795482e18f5272f2e73" +dependencies = [ + "rustc-hex", +] + +[[package]] +name = "rustc-hex" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e75f6a532d0fd9f7f13144f392b6ad56a32696bfcd9c78f797f16bbb6f072d6" + [[package]] name = "rustc_version" version = "0.4.1" @@ -1061,7 +1705,7 @@ version = "0.38.37" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8acb788b847c24f28525660c4d7758620a7210875711f79e7f663cc152726811" dependencies = [ - "bitflags", + "bitflags 2.6.0", "errno", "libc", "linux-raw-sys", @@ -1107,7 +1751,7 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] @@ -1119,6 +1763,7 @@ dependencies = [ "base16ct", "der", "generic-array", + "pkcs8", "subtle", "zeroize", ] @@ -1131,13 +1776,22 @@ checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" [[package]] name = "serde" -version = "1.0.210" +version = "1.0.213" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" +checksum = "3ea7893ff5e2466df8d720bb615088341b295f849602c6956047f8f80f0e9bc1" dependencies = [ "serde_derive", ] +[[package]] +name = "serde-json-wasm" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e9213a07d53faa0b8dd81e767a54a8188a242fdb9be99ab75ec576a774bfdd7" +dependencies = [ + "serde", +] + [[package]] name = "serde-json-wasm" version = "1.0.1" @@ -1149,13 +1803,13 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.210" +version = "1.0.213" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" +checksum = "7e85ad2009c50b58e87caa8cd6dac16bdf511bbfb7af6c33df902396aa480fa5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] @@ -1166,14 +1820,14 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] name = "serde_json" -version = "1.0.128" +version = "1.0.132" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8" +checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03" dependencies = [ "itoa", "memchr", @@ -1181,6 +1835,30 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_repr" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.85", +] + +[[package]] +name = "sha2" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" +dependencies = [ + "block-buffer 0.9.0", + "cfg-if", + "cpufeatures", + "digest 0.9.0", + "opaque-debug", +] + [[package]] name = "sha2" version = "0.10.8" @@ -1189,7 +1867,7 @@ checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" dependencies = [ "cfg-if", "cpufeatures", - "digest", + "digest 0.10.7", ] [[package]] @@ -1198,10 +1876,26 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ - "digest", - "rand_core", + "digest 0.10.7", + "rand_core 0.6.4", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", ] +[[package]] +name = "static_assertions" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c19be23126415861cb3a23e501d34a708f7f9b2183c5252d690941c2e69199d5" + [[package]] name = "static_assertions" version = "1.1.0" @@ -1214,6 +1908,15 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" +[[package]] +name = "subtle-encoding" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dcb1ed7b8330c5eed5441052651dd7a12c75e2ed88f2ec024ae1fa3a5e59945" +dependencies = [ + "zeroize", +] + [[package]] name = "syn" version = "1.0.109" @@ -1227,9 +1930,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.79" +version = "2.0.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89132cd0bf050864e1d38dc3bbc07a0eb8e7530af26344d3d2bbbef83499f590" +checksum = "5023162dfcd14ef8f32034d8bcd4cc5ddc61ef7a247c024a33e24e1f24d21b56" dependencies = [ "proc-macro2", "quote", @@ -1267,7 +1970,7 @@ dependencies = [ "cfg-if", "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] @@ -1278,28 +1981,37 @@ checksum = "5c89e72a01ed4c579669add59014b9a524d609c0c88c6a585ce37485879f6ffb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.85", "test-case-core", ] [[package]] name = "thiserror" -version = "1.0.64" +version = "1.0.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84" +checksum = "5d11abd9594d9b38965ef50805c5e469ca9cc6f197f883f717e0269a3057b3d5" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.64" +version = "1.0.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3" +checksum = "ae71770322cbd277e69d762a16c444af02aa0575ac0d174f0b9562d3b37f8602" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.85", +] + +[[package]] +name = "tiny-keccak" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d8a021c69bb74a44ccedb824a046447e2c84a01df9e5c20779750acb38e11b2" +dependencies = [ + "crunchy", ] [[package]] @@ -1308,6 +2020,30 @@ version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +[[package]] +name = "uint" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "082df6964410f6aa929a61ddfafc997e4f32c62c22490e439ac351cec827f436" +dependencies = [ + "byteorder", + "crunchy", + "heapsize", + "rustc-hex", +] + +[[package]] +name = "uint" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76f64bba2c53b04fcab63c01a7d7427eadc821e3bc48c34dc9ba29c501164b52" +dependencies = [ + "byteorder", + "crunchy", + "hex", + "static_assertions 1.1.0", +] + [[package]] name = "uint" version = "0.10.0" @@ -1317,7 +2053,7 @@ dependencies = [ "byteorder", "crunchy", "hex", - "static_assertions", + "static_assertions 1.1.0", ] [[package]] @@ -1359,6 +2095,28 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + [[package]] name = "windows-sys" version = "0.52.0" @@ -1459,7 +2217,7 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] @@ -1479,5 +2237,5 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.85", ] diff --git a/Cargo.toml b/Cargo.toml index 99f1e95..a04f053 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,20 +2,14 @@ resolver = "2" members = [ "packages/*", - # "contracts/factory", + "contracts/factory", "contracts/pair", - # "contracts/pair_stable", - # "contracts/pair_concentrated", - # "contracts/pair_astro_converter", - # "contracts/pair_transmuter", - # # "contracts/pair_concentrated_inj", TODO: rewrite OB liquidity deployment - # "contracts/pair_xyk_sale_tax", - # "contracts/router", - # "contracts/whitelist", - # "contracts/tokenomics/*", - # "contracts/periphery/*" + "contracts/pair_stable", + "contracts/router", ] +exclude = ["packages/palomadex_mocks"] + [profile.release] opt-level = 3 debug = false @@ -28,14 +22,13 @@ incremental = false overflow-checks = true [workspace.dependencies] -cosmwasm-schema = "2.1.4" -cosmwasm-std = "2.1.4" -cw-storage-plus = "2.0.0" -cw2 = "2.0.0" -cw20 = "2.0.0" -cw-utils = "2.0.0" +cosmwasm-schema = "1.5" +cosmwasm-std = "1.5" +cw-storage-plus = "1.2" +cw20 = "1.1.2" +cw-utils = "1.0.3" itertools = "0.13.0" schemars = "0.8.21" serde = { version = "1.0.210", default-features = false, features = ["derive"] } thiserror = { version = "1.0.64" } -palomadex = { path = "./packages/palomadex" } +palomadex = { path = "./packages/palomadex" } \ No newline at end of file diff --git a/artifacts/checksums.txt b/artifacts/checksums.txt new file mode 100644 index 0000000..c139a6c --- /dev/null +++ b/artifacts/checksums.txt @@ -0,0 +1,4 @@ +9d3f50466ea66ab22dc6b5680359bb625e1a9632f7d66db4af1b3042dbc53e59 palomadex_factory.wasm +6aae833b725bd57733ea140ef613524b183bafb2f53c6e12098df0bb1daad08d palomadex_pair.wasm +50afef59cfaa34e450f0611472bf7295f2bd0a7873cf7b8fbf697df606c58b7f palomadex_pair_stable.wasm +d0d43d09d863b7f16664e6292b193d6c6ba1607aed4577c4cf1b50bf3f133ef1 palomadex_router.wasm diff --git a/artifacts/palomadex_factory.wasm b/artifacts/palomadex_factory.wasm new file mode 100644 index 0000000..d80d757 Binary files /dev/null and b/artifacts/palomadex_factory.wasm differ diff --git a/artifacts/palomadex_pair.wasm b/artifacts/palomadex_pair.wasm new file mode 100644 index 0000000..de802d8 Binary files /dev/null and b/artifacts/palomadex_pair.wasm differ diff --git a/artifacts/palomadex_pair_stable.wasm b/artifacts/palomadex_pair_stable.wasm new file mode 100644 index 0000000..efd96c7 Binary files /dev/null and b/artifacts/palomadex_pair_stable.wasm differ diff --git a/artifacts/palomadex_router.wasm b/artifacts/palomadex_router.wasm new file mode 100644 index 0000000..4aebfab Binary files /dev/null and b/artifacts/palomadex_router.wasm differ diff --git a/contracts/factory/.cargo/config.toml b/contracts/factory/.cargo/config.toml new file mode 100644 index 0000000..23622ad --- /dev/null +++ b/contracts/factory/.cargo/config.toml @@ -0,0 +1,6 @@ +[alias] +wasm = "build --release --target wasm32-unknown-unknown" +wasm-debug = "build --target wasm32-unknown-unknown" +unit-test = "test --lib" +integration-test = "test --test integration" +schema = "run --example factory_schema" diff --git a/contracts/factory/.editorconfig b/contracts/factory/.editorconfig new file mode 100644 index 0000000..3d36f20 --- /dev/null +++ b/contracts/factory/.editorconfig @@ -0,0 +1,11 @@ +root = true + +[*] +indent_style = space +indent_size = 2 +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.rs] +indent_size = 4 diff --git a/contracts/factory/Cargo.toml b/contracts/factory/Cargo.toml index 292d72e..a340090 100644 --- a/contracts/factory/Cargo.toml +++ b/contracts/factory/Cargo.toml @@ -12,15 +12,26 @@ crate-type = ["cdylib", "rlib"] [features] # use library feature to disable all instantiate/execute/query exports library = [] +backtraces = ["cosmwasm-std/backtraces"] + +[lints.rust] +unexpected_cfgs = { level = "warn", check-cfg = ["cfg(tarpaulin_include)"] } [dependencies] cosmwasm-schema.workspace = true cosmwasm-std.workspace = true cw-storage-plus.workspace = true -cw2.workspace = true +cw20.workspace = true schemars.workspace = true serde.workspace = true thiserror.workspace = true +cw-utils.workspace = true +itertools.workspace = true +palomadex = { path = "../../packages/palomadex"} [dev-dependencies] -cw-multi-test = "2.0.0" +cw-multi-test = "1.2.0" +cw20-base = "1.1" +prost = "0.13.3" +anyhow = "1.0" +palomadex-pair = { path = "../pair" } \ No newline at end of file diff --git a/contracts/factory/README.md b/contracts/factory/README.md new file mode 100644 index 0000000..100a854 --- /dev/null +++ b/contracts/factory/README.md @@ -0,0 +1,245 @@ +# Palomadex Factory + +The factory contract can create new Palomadex pair contracts (and associated LP token contracts) and it is used as a directory for all pairs. The default pair types are constant product and stableswap but governance may decide to add custom pools that can have any implementation. + +--- + +## InstantiateMsg + +The instantiation message takes in the token code ID for the token type supported on Palomadex. It also takes in the `fee_address` that collects fees for governance, the contract `owner`, the Generator contract address and the initial pair types available to create. + +```json +{ + "token_code_id": 123, + "fee_address": "terra...", + "owner": "terra...", + "generator_address": "terra...", + "pair_configs": [{ + "code_id": 123, + "pair_type": { + "xyk": {} + }, + "total_fee_bps": 100, + "maker_fee_bps": 10, + "is_disabled": false + } + ] +} +``` + +## ExecuteMsg + +### `update_config` + +Updates contract variables, namely the code ID of the token implementation used in Palomadex, the address that receives governance fees and the Generator contract address. + +```json +{ + "update_config": { + "token_code_id": 123, + "fee_address": "terra...", + "generator_address": "terra..." + } +} +``` + +### `update_pair_config` + +This function can be used to: + +- Update the code ID used to instantiate new pairs of a specific type +- Change the fee structure for a pair +- Disable the pair type so no other pairs can be instantiated + +Note that all fields are optional. + +The fee structure for a pair is set up as follows: + +- `total_fee_bps` is the total amount of fees (in bps) that are charged on each swap +- `maker_fee_bps` is the percentage of fees out of `total_fee_bps` that is sent to governance. 100% is 10,000 + +As an example, let's say a pool charged 30bps (`total_fee_bps` is 30) and we want 1/3r of the fees to go to governance. In this case, `maker_fee_bps` should be 3333 because 3333 / 10,000 * 30 / 100 = 0.1% + +```json +{ + "update_pair_config": { + "config": { + "code_id": 123, + "pair_type": { + "xyk": {} + }, + "total_fee_bps": 100, + "maker_fee_bps": 10, + "is_disabled": false + } + } +} +``` + +### `create_pair` + +Anyone can execute this function to create an Palomadex pair. `CreatePair` creates both a `Pair` contract and a `LP(liquidity provider)` token contract. The account that instantiates the pair must specify the pair type they want as well as the assets for which the pool is created. + +Custom pool types may also need extra parameters which can be packed in `init_params`. + +```json +{ + "create_pair": { + "pair_type": { + "xyk": {} + }, + "asset_infos": [ + { + "token": { + "contract_addr": "terra..." + } + }, + { + "native_token": { + "denom": "uusd" + } + } + ], + "init_params": "" + } +} +``` + +### `deregister` + +Deregisters an already registered pair. This allows someone else to create a new pair (of any type) for the tokens that don't have a registered pair anymore. This is how pairs can be "upgraded". + +```json +{ + "deregister": { + "asset_infos": [ + { + "token": { + "contract_address": "terra..." + } + }, + { + "native_token": { + "denom": "uusd" + } + } + ] + } +} +``` + +### `propose_new_owner` + +Creates an offer to change the contract ownership. The validity period of the offer is set in the `expires_in` variable. After `expires_in` seconds pass, the proposal expires and cannot be accepted anymore. + +```json +{ + "propose_new_owner": { + "owner": "terra...", + "expires_in": 1234567 + } +} +``` + +### `drop_ownership_proposal` + +Removes an existing offer to change the contract owner. + +```json +{ + "drop_ownership_proposal": {} +} +``` + +### `claim_ownership` + +Used to claim contract ownership. + +```json +{ + "claim_ownership": {} +} +``` + +## QueryMsg + +All query messages are described below. A custom struct is defined for each query response. + +### `config` + +Returns general factory parameters (owner, token code ID, pair type configurations). + +```json +{ + "config": {} +} +``` + +### `pair` + +Returns information about a specific pair. + +```json +{ + "pair": { + "asset_infos": [ + { + "token": { + "contract_address": "terra..." + } + }, + { + "native_token": { + "denom": "uusd" + } + } + ] + } +} +``` + +### `pairs` + +Returns information about multiple pairs (the result is paginated). The function starts returning pair information starting after the pair `start_after`. The function returns maximum `limit` pairs. + +```json +{ + "pairs": { + "start_after": [ + { + "token": { + "contract_address": "terra..." + } + }, + { + "native_token": { + "denom": "uusd" + } + } + ], + "limit": 10 + } +} +``` + +### `fee_info` + +Returns the fee information for a specific pair type (`total_fee_bps` and `maker_fee_bps`). + +```json +{ + "pair_type": { + "xyk": {} + } +} +``` + +### `blacklisted_pair_types` + +Returns a vector that contains blacklisted pair types. + +```json +{ + "blacklisted_pair_types": {} +} +``` diff --git a/contracts/factory/examples/factory_schema.rs b/contracts/factory/examples/factory_schema.rs new file mode 100644 index 0000000..4bbd1ca --- /dev/null +++ b/contracts/factory/examples/factory_schema.rs @@ -0,0 +1,10 @@ +use cosmwasm_schema::write_api; +use palomadex::factory::{ExecuteMsg, InstantiateMsg, QueryMsg}; + +fn main() { + write_api! { + instantiate: InstantiateMsg, + query: QueryMsg, + execute: ExecuteMsg, + } +} diff --git a/contracts/factory/src/contract.rs b/contracts/factory/src/contract.rs new file mode 100644 index 0000000..6e058cd --- /dev/null +++ b/contracts/factory/src/contract.rs @@ -0,0 +1,483 @@ +use std::collections::HashSet; + +#[cfg(not(feature = "library"))] +use cosmwasm_std::entry_point; +use cosmwasm_std::{ + attr, to_json_binary, Binary, Deps, DepsMut, Env, MessageInfo, Order, Reply, ReplyOn, Response, + StdError, StdResult, SubMsg, SubMsgResponse, SubMsgResult, WasmMsg, +}; +use cw_utils::parse_instantiate_response_data; +use itertools::Itertools; + +use palomadex::asset::{addr_opt_validate, AssetInfo, PairInfo}; +use palomadex::common::{claim_ownership, drop_ownership_proposal, propose_new_owner}; +use palomadex::factory::{ + Config, ConfigResponse, ExecuteMsg, FeeInfoResponse, InstantiateMsg, PairConfig, PairType, + PairsResponse, QueryMsg, +}; +use palomadex::pair::InstantiateMsg as PairInstantiateMsg; + +use crate::error::ContractError; +use crate::querier::query_pair_info; +use crate::state::{ + check_asset_infos, pair_key, read_pairs, TmpPairInfo, CONFIG, OWNERSHIP_PROPOSAL, PAIRS, + PAIR_CONFIGS, TMP_PAIR_INFO, +}; + +/// A `reply` call code ID used in a sub-message. +const INSTANTIATE_PAIR_REPLY_ID: u64 = 1; + +/// Creates a new contract with the specified parameters packed in the `msg` variable. +/// +/// * **msg** is message which contains the parameters used for creating the contract. +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn instantiate( + deps: DepsMut, + _env: Env, + _info: MessageInfo, + msg: InstantiateMsg, +) -> Result { + let mut config = Config { + owner: deps.api.addr_validate(&msg.owner)?, + token_code_id: msg.token_code_id, + fee_address: None, + generator_address: None, + whitelist_code_id: msg.whitelist_code_id, + coin_registry_address: deps.api.addr_validate(&msg.coin_registry_address)?, + }; + + config.generator_address = addr_opt_validate(deps.api, &msg.generator_address)?; + + config.fee_address = addr_opt_validate(deps.api, &msg.fee_address)?; + + let config_set: HashSet = msg + .pair_configs + .iter() + .map(|pc| pc.pair_type.to_string()) + .collect(); + + if config_set.len() != msg.pair_configs.len() { + return Err(ContractError::PairConfigDuplicate {}); + } + + for pc in msg.pair_configs.iter() { + // Validate total and maker fee bps + if !pc.valid_fee_bps() { + return Err(ContractError::PairConfigInvalidFeeBps {}); + } + PAIR_CONFIGS.save(deps.storage, pc.pair_type.to_string(), pc)?; + } + CONFIG.save(deps.storage, &config)?; + + Ok(Response::new()) +} + +/// Data structure used to update general contract parameters. +pub struct UpdateConfig { + /// This is the CW20 token contract code identifier + token_code_id: Option, + /// Contract address to send governance fees to (the Maker) + fee_address: Option, + /// Generator contract address + generator_address: Option, + /// CW1 whitelist contract code id used to store 3rd party staking rewards + whitelist_code_id: Option, + coin_registry_address: Option, +} + +/// Exposes all the execute functions available in the contract. +/// * **msg** is an object of type [`ExecuteMsg`]. +/// +/// ## Variants +/// * **ExecuteMsg::UpdateConfig { +/// token_code_id, +/// fee_address, +/// generator_address, +/// }** Updates general contract parameters. +/// +/// * **ExecuteMsg::UpdatePairConfig { config }** Updates a pair type +/// * configuration or creates a new pair type if a [`Custom`] name is used (which hasn't been used before). +/// +/// * **ExecuteMsg::CreatePair { +/// pair_type, +/// asset_infos, +/// init_params, +/// }** Creates a new pair with the specified input parameters. +/// +/// * **ExecuteMsg::Deregister { asset_infos }** Removes an existing pair from the factory. +/// * The asset information is for the assets that are traded in the pair. +/// +/// * **ExecuteMsg::ProposeNewOwner { owner, expires_in }** Creates a request to change contract ownership. +/// +/// * **ExecuteMsg::DropOwnershipProposal {}** Removes a request to change contract ownership. +/// +/// * **ExecuteMsg::ClaimOwnership {}** Claims contract ownership. +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn execute( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: ExecuteMsg, +) -> Result { + match msg { + ExecuteMsg::UpdateConfig { + token_code_id, + fee_address, + generator_address, + whitelist_code_id, + coin_registry_address, + } => execute_update_config( + deps, + info, + UpdateConfig { + token_code_id, + fee_address, + generator_address, + whitelist_code_id, + coin_registry_address, + }, + ), + ExecuteMsg::UpdatePairConfig { config } => execute_update_pair_config(deps, info, config), + ExecuteMsg::CreatePair { + pair_type, + asset_infos, + init_params, + } => execute_create_pair(deps, info, env, pair_type, asset_infos, init_params), + ExecuteMsg::Deregister { asset_infos } => deregister(deps, info, asset_infos), + ExecuteMsg::ProposeNewOwner { owner, expires_in } => { + let config = CONFIG.load(deps.storage)?; + + propose_new_owner( + deps, + info, + env, + owner, + expires_in, + config.owner, + OWNERSHIP_PROPOSAL, + ) + .map_err(Into::into) + } + ExecuteMsg::DropOwnershipProposal {} => { + let config = CONFIG.load(deps.storage)?; + + drop_ownership_proposal(deps, info, config.owner, OWNERSHIP_PROPOSAL) + .map_err(Into::into) + } + ExecuteMsg::ClaimOwnership {} => { + claim_ownership(deps, info, env, OWNERSHIP_PROPOSAL, |deps, new_owner| { + CONFIG + .update::<_, StdError>(deps.storage, |mut v| { + v.owner = new_owner; + Ok(v) + }) + .map(|_| ()) + }) + .map_err(Into::into) + } + } +} + +/// Updates general contract settings. +/// +/// * **param** is an object of type [`UpdateConfig`] that contains the parameters to update. +/// +/// ## Executor +/// Only the owner can execute this. +pub fn execute_update_config( + deps: DepsMut, + info: MessageInfo, + param: UpdateConfig, +) -> Result { + let mut config = CONFIG.load(deps.storage)?; + + // Permission check + if info.sender != config.owner { + return Err(ContractError::Unauthorized {}); + } + + if let Some(fee_address) = param.fee_address { + // Validate address format + config.fee_address = Some(deps.api.addr_validate(&fee_address)?); + } + + if let Some(generator_address) = param.generator_address { + // Validate the address format + config.generator_address = Some(deps.api.addr_validate(&generator_address)?); + } + + if let Some(token_code_id) = param.token_code_id { + config.token_code_id = token_code_id; + } + + if let Some(code_id) = param.whitelist_code_id { + config.whitelist_code_id = code_id; + } + + if let Some(coin_registry_address) = param.coin_registry_address { + config.coin_registry_address = deps.api.addr_validate(&coin_registry_address)?; + } + + CONFIG.save(deps.storage, &config)?; + + Ok(Response::new().add_attribute("action", "update_config")) +} + +/// Updates a pair type's configuration. +/// +/// * **pair_config** is an object of type [`PairConfig`] that contains the pair type information to update. +/// +/// ## Executor +/// Only the owner can execute this. +pub fn execute_update_pair_config( + deps: DepsMut, + info: MessageInfo, + pair_config: PairConfig, +) -> Result { + let config = CONFIG.load(deps.storage)?; + + // Permission check + if info.sender != config.owner { + return Err(ContractError::Unauthorized {}); + } + + // Validate total and maker fee bps + if !pair_config.valid_fee_bps() { + return Err(ContractError::PairConfigInvalidFeeBps {}); + } + + PAIR_CONFIGS.save( + deps.storage, + pair_config.pair_type.to_string(), + &pair_config, + )?; + + Ok(Response::new().add_attribute("action", "update_pair_config")) +} + +/// Creates a new pair of `pair_type` with the assets specified in `asset_infos`. +/// +/// * **pair_type** is the pair type of the newly created pair. +/// +/// * **asset_infos** is a vector with assets for which we create a pair. +/// +/// * **init_params** These are packed params used for custom pair types that need extra data to be instantiated. +pub fn execute_create_pair( + deps: DepsMut, + info: MessageInfo, + env: Env, + pair_type: PairType, + asset_infos: Vec, + init_params: Option, +) -> Result { + check_asset_infos(deps.api, &asset_infos)?; + + let config = CONFIG.load(deps.storage)?; + + if PAIRS.has(deps.storage, &pair_key(&asset_infos)) { + return Err(ContractError::PairWasCreated {}); + } + + // Get pair type from config + let pair_config = PAIR_CONFIGS + .load(deps.storage, pair_type.to_string()) + .map_err(|_| ContractError::PairConfigNotFound {})?; + + if pair_config.permissioned && info.sender != config.owner { + return Err(ContractError::Unauthorized {}); + } + + // Check if pair config is disabled + if pair_config.is_disabled { + return Err(ContractError::PairConfigDisabled {}); + } + + let pair_key = pair_key(&asset_infos); + TMP_PAIR_INFO.save(deps.storage, &TmpPairInfo { pair_key })?; + + let sub_msg: Vec = vec![SubMsg { + id: INSTANTIATE_PAIR_REPLY_ID, + msg: WasmMsg::Instantiate { + admin: Some(config.owner.to_string()), + code_id: pair_config.code_id, + msg: to_json_binary(&PairInstantiateMsg { + asset_infos: asset_infos.clone(), + token_code_id: config.token_code_id, + factory_addr: env.contract.address.to_string(), + init_params, + })?, + funds: vec![], + label: "Palomadex pair".to_string(), + } + .into(), + gas_limit: None, + reply_on: ReplyOn::Success, + }]; + + Ok(Response::new() + .add_submessages(sub_msg) + .add_attributes(vec![ + attr("action", "create_pair"), + attr("pair", asset_infos.iter().join("-")), + ])) +} + +/// The entry point to the contract for processing replies from submessages. +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn reply(deps: DepsMut, _env: Env, msg: Reply) -> Result { + match msg { + Reply { + id: INSTANTIATE_PAIR_REPLY_ID, + result: + SubMsgResult::Ok(SubMsgResponse { + data: Some(data), .. + }), + } => { + let tmp = TMP_PAIR_INFO.load(deps.storage)?; + if PAIRS.has(deps.storage, &tmp.pair_key) { + return Err(ContractError::PairWasRegistered {}); + } + + let init_response = parse_instantiate_response_data(data.as_slice()) + .map_err(|e| StdError::generic_err(format!("{e}")))?; + + let pair_contract = deps.api.addr_validate(&init_response.contract_address)?; + + PAIRS.save(deps.storage, &tmp.pair_key, &pair_contract)?; + + Ok(Response::new().add_attributes(vec![ + attr("action", "register"), + attr("pair_contract_addr", pair_contract), + ])) + } + _ => Err(ContractError::FailedToParseReply {}), + } +} + +/// Removes an existing pair from the factory. +/// +/// * **asset_infos** is a vector with assets for which we deregister the pair. +/// +/// ## Executor +/// Only the owner can execute this. +pub fn deregister( + deps: DepsMut, + info: MessageInfo, + asset_infos: Vec, +) -> Result { + check_asset_infos(deps.api, &asset_infos)?; + + let config = CONFIG.load(deps.storage)?; + + if info.sender != config.owner { + return Err(ContractError::Unauthorized {}); + } + + let pair_addr = PAIRS.load(deps.storage, &pair_key(&asset_infos))?; + PAIRS.remove(deps.storage, &pair_key(&asset_infos)); + + Ok(Response::new().add_attributes(vec![ + attr("action", "deregister"), + attr("pair_contract_addr", pair_addr), + ])) +} + +/// Exposes all the queries available in the contract. +/// +/// ## Queries +/// * **QueryMsg::Config {}** Returns general contract parameters using a custom [`ConfigResponse`] structure. +/// +/// * **QueryMsg::Pair { asset_infos }** Returns a [`PairInfo`] object with information about a specific Palomadex pair. +/// +/// * **QueryMsg::Pairs { start_after, limit }** Returns an array that contains items of type [`PairInfo`]. +/// This returns information about multiple Palomadex pairs +/// +/// * **QueryMsg::FeeInfo { pair_type }** Returns the fee structure (total and maker fees) for a specific pair type. +/// +/// * **QueryMsg::BlacklistedPairTypes {}** Returns a vector that contains blacklisted pair types (pair types that cannot get ASTRO emissions). +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { + match msg { + QueryMsg::Config {} => to_json_binary(&query_config(deps)?), + QueryMsg::Pair { asset_infos } => to_json_binary(&query_pair(deps, asset_infos)?), + QueryMsg::Pairs { start_after, limit } => { + to_json_binary(&query_pairs(deps, start_after, limit)?) + } + QueryMsg::FeeInfo { pair_type } => to_json_binary(&query_fee_info(deps, pair_type)?), + QueryMsg::BlacklistedPairTypes {} => to_json_binary(&query_blacklisted_pair_types(deps)?), + } +} + +/// Returns a vector that contains blacklisted pair types +pub fn query_blacklisted_pair_types(deps: Deps) -> StdResult> { + PAIR_CONFIGS + .range(deps.storage, None, None, Order::Ascending) + .filter_map(|result| match result { + Ok(v) => { + if v.1.is_disabled || v.1.is_generator_disabled { + Some(Ok(v.1.pair_type)) + } else { + None + } + } + Err(e) => Some(Err(e)), + }) + .collect() +} + +/// Returns general contract parameters using a custom [`ConfigResponse`] structure. +pub fn query_config(deps: Deps) -> StdResult { + let config = CONFIG.load(deps.storage)?; + let resp = ConfigResponse { + owner: config.owner, + token_code_id: config.token_code_id, + pair_configs: PAIR_CONFIGS + .range(deps.storage, None, None, Order::Ascending) + .map(|item| Ok(item?.1)) + .collect::>>()?, + fee_address: config.fee_address, + generator_address: config.generator_address, + whitelist_code_id: config.whitelist_code_id, + coin_registry_address: config.coin_registry_address, + }; + + Ok(resp) +} + +/// Returns a pair's data using the assets in `asset_infos` as input (those being the assets that are traded in the pair). +/// * **asset_infos** is a vector with assets traded in the pair. +pub fn query_pair(deps: Deps, asset_infos: Vec) -> StdResult { + let pair_addr = PAIRS.load(deps.storage, &pair_key(&asset_infos))?; + query_pair_info(&deps.querier, pair_addr) +} + +/// Returns a vector with pair data that contains items of type [`PairInfo`]. Querying starts at `start_after` and returns `limit` pairs. +/// * **start_after** is a field which accepts a vector with items of type [`AssetInfo`]. +/// This is the pair from which we start a query. +/// +/// * **limit** sets the number of pairs to be retrieved. +pub fn query_pairs( + deps: Deps, + start_after: Option>, + limit: Option, +) -> StdResult { + let pairs = read_pairs(deps, start_after, limit)? + .iter() + .map(|pair_addr| query_pair_info(&deps.querier, pair_addr)) + .collect::>>()?; + + Ok(PairsResponse { pairs }) +} + +/// Returns the fee setup for a specific pair type using a [`FeeInfoResponse`] struct. +/// * **pair_type** is a struct that represents the fee information (total and maker fees) for a specific pair type. +pub fn query_fee_info(deps: Deps, pair_type: PairType) -> StdResult { + let config = CONFIG.load(deps.storage)?; + let pair_config = PAIR_CONFIGS.load(deps.storage, pair_type.to_string())?; + + Ok(FeeInfoResponse { + fee_address: config.fee_address, + total_fee_bps: pair_config.total_fee_bps, + maker_fee_bps: pair_config.maker_fee_bps, + }) +} diff --git a/contracts/factory/src/error.rs b/contracts/factory/src/error.rs new file mode 100644 index 0000000..b249520 --- /dev/null +++ b/contracts/factory/src/error.rs @@ -0,0 +1,39 @@ +use cosmwasm_std::StdError; +use thiserror::Error; + +/// This enum describes factory contract errors +#[derive(Error, Debug, PartialEq)] +pub enum ContractError { + #[error("{0}")] + Std(#[from] StdError), + + #[error("Unauthorized")] + Unauthorized {}, + + #[error("Pair was already created")] + PairWasCreated {}, + + #[error("Pair was already registered")] + PairWasRegistered {}, + + #[error("Duplicate of pair configs")] + PairConfigDuplicate {}, + + #[error("Fee bps in pair config must be smaller than or equal to 10,000")] + PairConfigInvalidFeeBps {}, + + #[error("Pair config not found")] + PairConfigNotFound {}, + + #[error("Pair config disabled")] + PairConfigDisabled {}, + + #[error("Doubling assets in asset infos")] + DoublingAssets {}, + + #[error("Contract can't be migrated!")] + MigrationError {}, + + #[error("Failed to parse or process reply message")] + FailedToParseReply {}, +} diff --git a/contracts/factory/src/lib.rs b/contracts/factory/src/lib.rs index e69de29..8056178 100644 --- a/contracts/factory/src/lib.rs +++ b/contracts/factory/src/lib.rs @@ -0,0 +1,12 @@ +pub mod contract; +pub mod state; + +pub mod error; + +mod querier; + +#[cfg(test)] +mod testing; + +#[cfg(test)] +mod mock_querier; diff --git a/contracts/factory/src/mock_querier.rs b/contracts/factory/src/mock_querier.rs new file mode 100644 index 0000000..ecd0b57 --- /dev/null +++ b/contracts/factory/src/mock_querier.rs @@ -0,0 +1,107 @@ +use std::collections::HashMap; + +use cosmwasm_std::testing::{MockApi, MockQuerier, MockStorage, MOCK_CONTRACT_ADDR}; +use cosmwasm_std::{ + from_json, to_json_binary, Coin, Empty, OwnedDeps, Querier, QuerierResult, QueryRequest, + SystemError, SystemResult, WasmQuery, +}; + +use palomadex::asset::PairInfo; +use palomadex::pair::QueryMsg; + +/// mock_dependencies is a drop-in replacement for cosmwasm_std::testing::mock_dependencies. +/// This uses the Palomadex CustomQuerier. +pub fn mock_dependencies( + contract_balance: &[Coin], +) -> OwnedDeps { + let custom_querier: WasmMockQuerier = + WasmMockQuerier::new(MockQuerier::new(&[(MOCK_CONTRACT_ADDR, contract_balance)])); + + OwnedDeps { + storage: MockStorage::default(), + api: MockApi::default(), + querier: custom_querier, + custom_query_type: Default::default(), + } +} + +pub struct WasmMockQuerier { + base: MockQuerier, + palomadex_pair_querier: PalomadexPairQuerier, +} + +#[derive(Clone, Default)] +pub struct PalomadexPairQuerier { + pairs: HashMap, +} + +impl PalomadexPairQuerier { + pub fn new(pairs: &[(&String, &PairInfo)]) -> Self { + PalomadexPairQuerier { + pairs: pairs_to_map(pairs), + } + } +} + +pub(crate) fn pairs_to_map(pairs: &[(&String, &PairInfo)]) -> HashMap { + let mut pairs_map: HashMap = HashMap::new(); + for (key, pair) in pairs.iter() { + pairs_map.insert(key.to_string(), (*pair).clone()); + } + pairs_map +} + +impl Querier for WasmMockQuerier { + fn raw_query(&self, bin_request: &[u8]) -> QuerierResult { + // MockQuerier doesn't support Custom, so we ignore it completely + let request: QueryRequest = match from_json(bin_request) { + Ok(v) => v, + Err(e) => { + return SystemResult::Err(SystemError::InvalidRequest { + error: format!("Parsing query request: {}", e), + request: bin_request.into(), + }) + } + }; + self.handle_query(&request) + } +} + +impl WasmMockQuerier { + pub fn handle_query(&self, request: &QueryRequest) -> QuerierResult { + match &request { + QueryRequest::Wasm(WasmQuery::Smart {contract_addr, msg})// => { + => match from_json(&msg).unwrap() { + QueryMsg::Pair {} => { + let pair_info: PairInfo = + match self.palomadex_pair_querier.pairs.get(contract_addr) { + Some(v) => v.clone(), + None => { + return SystemResult::Err(SystemError::NoSuchContract { + addr: contract_addr.clone(), + }) + } + }; + + SystemResult::Ok(to_json_binary(&pair_info).into()) + } + _ => panic!("DO NOT ENTER HERE") + } + _ => self.base.handle_query(request), + } + } +} + +impl WasmMockQuerier { + pub fn new(base: MockQuerier) -> Self { + WasmMockQuerier { + base, + palomadex_pair_querier: PalomadexPairQuerier::default(), + } + } + + // Configure the Palomadex pair + pub fn with_palomadex_pairs(&mut self, pairs: &[(&String, &PairInfo)]) { + self.palomadex_pair_querier = PalomadexPairQuerier::new(pairs); + } +} diff --git a/contracts/factory/src/querier.rs b/contracts/factory/src/querier.rs new file mode 100644 index 0000000..8823091 --- /dev/null +++ b/contracts/factory/src/querier.rs @@ -0,0 +1,13 @@ +use cosmwasm_std::{QuerierWrapper, StdResult}; +use palomadex::asset::PairInfo; +use palomadex::pair::QueryMsg; + +/// Returns information about a pair (using the [`PairInfo`] struct). +/// +/// `pair_contract` is the pair for which to retrieve information. +pub fn query_pair_info( + querier: &QuerierWrapper, + pair_contract: impl Into, +) -> StdResult { + querier.query_wasm_smart(pair_contract, &QueryMsg::Pair {}) +} diff --git a/contracts/factory/src/state.rs b/contracts/factory/src/state.rs new file mode 100644 index 0000000..be917cd --- /dev/null +++ b/contracts/factory/src/state.rs @@ -0,0 +1,178 @@ +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{Addr, Api, Deps, Order, StdResult}; +use cw_storage_plus::{Bound, Item, Map}; +use itertools::Itertools; + +use crate::error::ContractError; +use palomadex::asset::AssetInfo; +use palomadex::common::OwnershipProposal; +use palomadex::factory::{Config, PairConfig}; +/// This is an intermediate structure for storing a pair's key. It is used in a submessage response. +#[cw_serde] +pub struct TmpPairInfo { + pub pair_key: Vec, +} + +/// Saves a pair's key +pub const TMP_PAIR_INFO: Item = Item::new("tmp_pair_info"); + +/// Saves factory settings +pub const CONFIG: Item = Item::new("config"); + +/// Saves created pairs (from olders to latest) +pub const PAIRS: Map<&[u8], Addr> = Map::new("pair_info"); + +/// Calculates a pair key from the specified parameters in the `asset_infos` variable. +/// +/// `asset_infos` is an array with multiple items of type [`AssetInfo`]. +pub fn pair_key(asset_infos: &[AssetInfo]) -> Vec { + asset_infos + .iter() + .map(AssetInfo::as_bytes) + .sorted() + .flatten() + .copied() + .collect() +} + +/// Saves pair type configurations +pub const PAIR_CONFIGS: Map = Map::new("pair_configs"); + +/// ## Pagination settings +/// The maximum limit for reading pairs from [`PAIRS`] +const MAX_LIMIT: u32 = 30; +/// The default limit for reading pairs from [`PAIRS`] +const DEFAULT_LIMIT: u32 = 10; + +/// Reads pairs from the [`PAIRS`] vector according to the `start_after` and `limit` variables. +/// Otherwise, it returns the default number of pairs, starting from the oldest one. +/// +/// `start_after` is the pair from which the function starts to fetch results. +/// +/// `limit` is the number of items to retrieve. +pub fn read_pairs( + deps: Deps, + start_after: Option>, + limit: Option, +) -> StdResult> { + let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize; + + if let Some(start) = calc_range_start(start_after) { + PAIRS + .range( + deps.storage, + Some(Bound::exclusive(start.as_slice())), + None, + Order::Ascending, + ) + .take(limit) + .map(|item| { + let (_, pair_addr) = item?; + Ok(pair_addr) + }) + .collect() + } else { + PAIRS + .range(deps.storage, None, None, Order::Ascending) + .take(limit) + .map(|item| { + let (_, pair_addr) = item?; + Ok(pair_addr) + }) + .collect() + } +} + +/// Calculates the key of a pair from which to start reading data. +/// +/// `start_after` is an [`Option`] type that accepts [`AssetInfo`] elements. +/// It is the token pair which we use to determine the start index for a range when returning data for multiple pairs +fn calc_range_start(start_after: Option>) -> Option> { + start_after.map(|ref asset| { + let mut key = pair_key(asset); + key.push(1); + key + }) +} + +pub(crate) fn check_asset_infos( + api: &dyn Api, + asset_infos: &[AssetInfo], +) -> Result<(), ContractError> { + if !asset_infos.iter().all_unique() { + return Err(ContractError::DoublingAssets {}); + } + + asset_infos + .iter() + .try_for_each(|asset_info| asset_info.check(api)) + .map_err(Into::into) +} + +/// Stores the latest contract ownership transfer proposal +pub const OWNERSHIP_PROPOSAL: Item = Item::new("ownership_proposal"); + +/// This state key isn't used anymore but left for backward compatability with old pairs +pub const PAIRS_TO_MIGRATE: Item> = Item::new("pairs_to_migrate"); + +#[cfg(test)] +mod tests { + use palomadex::asset::{native_asset_info, token_asset_info}; + + use super::*; + + fn get_test_case() -> Vec<[AssetInfo; 2]> { + vec![ + [ + native_asset_info("uluna".to_string()), + native_asset_info("uusd".to_string()), + ], + [ + native_asset_info("uluna".to_string()), + token_asset_info(Addr::unchecked("astro_token_addr")), + ], + [ + token_asset_info(Addr::unchecked("random_token_addr")), + token_asset_info(Addr::unchecked("astro_token_addr")), + ], + ] + } + + #[test] + fn test_legacy_pair_key() { + fn legacy_pair_key(asset_infos: &[AssetInfo; 2]) -> Vec { + let mut asset_infos = asset_infos.to_vec(); + asset_infos.sort_by(|a, b| a.as_bytes().cmp(b.as_bytes())); + + [asset_infos[0].as_bytes(), asset_infos[1].as_bytes()].concat() + } + + for asset_infos in get_test_case() { + assert_eq!(legacy_pair_key(&asset_infos), pair_key(&asset_infos)); + } + } + + #[test] + fn test_legacy_start_after() { + fn legacy_calc_range_start(start_after: Option<[AssetInfo; 2]>) -> Option> { + start_after.map(|asset_infos| { + let mut asset_infos = asset_infos.to_vec(); + asset_infos.sort_by(|a, b| a.as_bytes().cmp(b.as_bytes())); + + let mut v = [asset_infos[0].as_bytes(), asset_infos[1].as_bytes()] + .concat() + .as_slice() + .to_vec(); + v.push(1); + v + }) + } + + for asset_infos in get_test_case() { + assert_eq!( + legacy_calc_range_start(Some(asset_infos.clone())), + calc_range_start(Some(asset_infos.to_vec())) + ); + } + } +} diff --git a/contracts/factory/src/testing.rs b/contracts/factory/src/testing.rs new file mode 100644 index 0000000..1f9c1a3 --- /dev/null +++ b/contracts/factory/src/testing.rs @@ -0,0 +1,774 @@ +use cosmwasm_std::{ + attr, from_json, to_json_binary, Addr, Reply, ReplyOn, SubMsg, SubMsgResponse, SubMsgResult, + WasmMsg, +}; + +use crate::mock_querier::mock_dependencies; +use crate::state::CONFIG; +use crate::{ + contract::{execute, instantiate, query}, + error::ContractError, +}; + +use palomadex::asset::{AssetInfo, PairInfo}; +use palomadex::factory::{ + ConfigResponse, ExecuteMsg, InstantiateMsg, PairConfig, PairType, PairsResponse, QueryMsg, +}; + +use crate::contract::reply; +use cosmwasm_std::testing::{mock_env, mock_info, MOCK_CONTRACT_ADDR}; +use palomadex::pair::InstantiateMsg as PairInstantiateMsg; + +use prost::Message; + +#[derive(Clone, PartialEq, Message)] +struct MsgInstantiateContractResponse { + #[prost(string, tag = "1")] + pub contract_address: String, + #[prost(bytes, tag = "2")] + pub data: Vec, +} + +#[test] +fn pair_type_to_string() { + assert_eq!(PairType::Xyk {}.to_string(), "xyk"); + assert_eq!(PairType::Stable {}.to_string(), "stable"); +} + +#[test] +fn proper_initialization() { + // Validate total and maker fee bps + let mut deps = mock_dependencies(&[]); + let owner = "owner0000".to_string(); + + let msg = InstantiateMsg { + pair_configs: vec![ + PairConfig { + code_id: 123u64, + pair_type: PairType::Xyk {}, + total_fee_bps: 100, + maker_fee_bps: 10, + is_disabled: false, + is_generator_disabled: false, + permissioned: false, + }, + PairConfig { + code_id: 325u64, + pair_type: PairType::Xyk {}, + total_fee_bps: 100, + maker_fee_bps: 10, + is_disabled: false, + is_generator_disabled: false, + permissioned: false, + }, + ], + token_code_id: 123u64, + fee_address: None, + generator_address: Some(String::from("generator")), + owner: owner.clone(), + whitelist_code_id: 234u64, + coin_registry_address: "coin_registry".to_string(), + }; + + let env = mock_env(); + let info = mock_info("addr0000", &[]); + + let res = instantiate(deps.as_mut(), env.clone(), info, msg).unwrap_err(); + assert_eq!(res, ContractError::PairConfigDuplicate {}); + + let msg = InstantiateMsg { + pair_configs: vec![PairConfig { + code_id: 123u64, + pair_type: PairType::Xyk {}, + total_fee_bps: 10_001, + maker_fee_bps: 10, + is_disabled: false, + is_generator_disabled: false, + permissioned: false, + }], + token_code_id: 123u64, + fee_address: None, + generator_address: Some(String::from("generator")), + owner: owner.clone(), + whitelist_code_id: 234u64, + coin_registry_address: "coin_registry".to_string(), + }; + + let env = mock_env(); + let info = mock_info("addr0000", &[]); + + let res = instantiate(deps.as_mut(), env.clone(), info, msg).unwrap_err(); + assert_eq!(res, ContractError::PairConfigInvalidFeeBps {}); + + let mut deps = mock_dependencies(&[]); + + let msg = InstantiateMsg { + pair_configs: vec![ + PairConfig { + code_id: 325u64, + pair_type: PairType::Stable {}, + total_fee_bps: 100, + maker_fee_bps: 10, + is_disabled: false, + is_generator_disabled: false, + permissioned: false, + }, + PairConfig { + code_id: 123u64, + pair_type: PairType::Xyk {}, + total_fee_bps: 100, + maker_fee_bps: 10, + is_disabled: false, + is_generator_disabled: false, + permissioned: false, + }, + ], + token_code_id: 123u64, + fee_address: None, + generator_address: Some(String::from("generator")), + owner: owner.clone(), + whitelist_code_id: 234u64, + coin_registry_address: "coin_registry".to_string(), + }; + + let env = mock_env(); + let info = mock_info("addr0000", &[]); + + instantiate(deps.as_mut(), env.clone(), info, msg.clone()).unwrap(); + + let query_res = query(deps.as_ref(), env, QueryMsg::Config {}).unwrap(); + let config_res: ConfigResponse = from_json(&query_res).unwrap(); + assert_eq!(123u64, config_res.token_code_id); + assert_eq!(msg.pair_configs, config_res.pair_configs); + assert_eq!(Addr::unchecked(owner), config_res.owner); +} + +#[test] +fn update_config() { + let mut deps = mock_dependencies(&[]); + let owner = "owner0000"; + + let pair_configs = vec![PairConfig { + code_id: 123u64, + pair_type: PairType::Xyk {}, + total_fee_bps: 3, + maker_fee_bps: 166, + is_disabled: false, + is_generator_disabled: false, + permissioned: false, + }]; + + let msg = InstantiateMsg { + pair_configs: pair_configs.clone(), + token_code_id: 123u64, + fee_address: None, + owner: owner.to_string(), + generator_address: Some(String::from("generator")), + whitelist_code_id: 234u64, + coin_registry_address: "coin_registry".to_string(), + }; + + let env = mock_env(); + let info = mock_info(owner, &[]); + + // We can just call .unwrap() to assert this was a success + let _res = instantiate(deps.as_mut(), env, info, msg).unwrap(); + + // Update config + let env = mock_env(); + let info = mock_info(owner, &[]); + let msg = ExecuteMsg::UpdateConfig { + token_code_id: Some(200u64), + fee_address: Some(String::from("new_fee_addr")), + generator_address: Some(String::from("new_generator_addr")), + whitelist_code_id: None, + coin_registry_address: None, + }; + + let res = execute(deps.as_mut(), env.clone(), info, msg).unwrap(); + assert_eq!(0, res.messages.len()); + + // It worked, let's query the state + let query_res = query(deps.as_ref(), env, QueryMsg::Config {}).unwrap(); + let config_res: ConfigResponse = from_json(&query_res).unwrap(); + assert_eq!(200u64, config_res.token_code_id); + assert_eq!(owner, config_res.owner); + assert_eq!( + String::from("new_fee_addr"), + config_res.fee_address.unwrap() + ); + assert_eq!( + String::from("new_generator_addr"), + config_res.generator_address.unwrap() + ); + + // Unauthorized err + let env = mock_env(); + let info = mock_info("addr0000", &[]); + let msg = ExecuteMsg::UpdateConfig { + token_code_id: None, + fee_address: None, + generator_address: None, + whitelist_code_id: None, + coin_registry_address: None, + }; + + let res = execute(deps.as_mut(), env.clone(), info, msg).unwrap_err(); + assert_eq!(res, ContractError::Unauthorized {}); +} + +#[test] +fn update_owner() { + let mut deps = mock_dependencies(&[]); + let owner = "owner0000"; + + let msg = InstantiateMsg { + pair_configs: vec![], + token_code_id: 123u64, + fee_address: None, + owner: owner.to_string(), + generator_address: Some(String::from("generator")), + whitelist_code_id: 234u64, + coin_registry_address: "coin_registry".to_string(), + }; + + let env = mock_env(); + let info = mock_info(owner, &[]); + + // We can just call .unwrap() to assert this was a success + instantiate(deps.as_mut(), env, info, msg).unwrap(); + + let new_owner = String::from("new_owner"); + + // New owner + let env = mock_env(); + let msg = ExecuteMsg::ProposeNewOwner { + owner: new_owner.clone(), + expires_in: 100, // seconds + }; + + let info = mock_info(new_owner.as_str(), &[]); + + // Unauthorized check + let err = execute(deps.as_mut(), env.clone(), info, msg.clone()).unwrap_err(); + assert_eq!(err.to_string(), "Generic error: Unauthorized"); + + // Claim before proposal + let info = mock_info(new_owner.as_str(), &[]); + execute( + deps.as_mut(), + env.clone(), + info, + ExecuteMsg::ClaimOwnership {}, + ) + .unwrap_err(); + + // Propose new owner + let info = mock_info(owner, &[]); + let res = execute(deps.as_mut(), env.clone(), info, msg).unwrap(); + assert_eq!(0, res.messages.len()); + + // Unauthorized ownership claim + let info = mock_info("invalid_addr", &[]); + let err = execute( + deps.as_mut(), + env.clone(), + info, + ExecuteMsg::ClaimOwnership {}, + ) + .unwrap_err(); + assert_eq!(err.to_string(), "Generic error: Unauthorized"); + + // Claim ownership + let info = mock_info(new_owner.as_str(), &[]); + let res = execute( + deps.as_mut(), + env.clone(), + info, + ExecuteMsg::ClaimOwnership {}, + ) + .unwrap(); + assert_eq!(0, res.messages.len()); + + // Let's query the state + let config: ConfigResponse = + from_json(&query(deps.as_ref(), env.clone(), QueryMsg::Config {}).unwrap()).unwrap(); + assert_eq!(new_owner, config.owner); +} + +#[test] +fn update_pair_config() { + let mut deps = mock_dependencies(&[]); + let owner = "owner0000"; + let pair_configs = vec![PairConfig { + code_id: 123u64, + pair_type: PairType::Xyk {}, + total_fee_bps: 100, + maker_fee_bps: 10, + is_disabled: false, + is_generator_disabled: false, + permissioned: false, + }]; + + let msg = InstantiateMsg { + pair_configs: pair_configs.clone(), + token_code_id: 123u64, + fee_address: None, + owner: owner.to_string(), + generator_address: Some(String::from("generator")), + whitelist_code_id: 234u64, + coin_registry_address: "coin_registry".to_string(), + }; + + let env = mock_env(); + let info = mock_info("addr0000", &[]); + + // We can just call .unwrap() to assert this was a success + instantiate(deps.as_mut(), env.clone(), info, msg).unwrap(); + + // It worked, let's query the state + let query_res = query(deps.as_ref(), env.clone(), QueryMsg::Config {}).unwrap(); + let config_res: ConfigResponse = from_json(&query_res).unwrap(); + assert_eq!(pair_configs, config_res.pair_configs); + + // Update config + let pair_config = PairConfig { + code_id: 800, + pair_type: PairType::Xyk {}, + total_fee_bps: 1, + maker_fee_bps: 2, + is_disabled: false, + is_generator_disabled: false, + permissioned: false, + }; + + // Unauthorized err + let env = mock_env(); + let info = mock_info("wrong-addr0000", &[]); + let msg = ExecuteMsg::UpdatePairConfig { + config: pair_config.clone(), + }; + + let res = execute(deps.as_mut(), env.clone(), info, msg).unwrap_err(); + assert_eq!(res, ContractError::Unauthorized {}); + + // Check validation of total and maker fee bps + let env = mock_env(); + let info = mock_info(owner, &[]); + let msg = ExecuteMsg::UpdatePairConfig { + config: PairConfig { + code_id: 123u64, + pair_type: PairType::Xyk {}, + total_fee_bps: 3, + maker_fee_bps: 10_001, + is_disabled: false, + is_generator_disabled: false, + permissioned: false, + }, + }; + + let res = execute(deps.as_mut(), env.clone(), info, msg).unwrap_err(); + assert_eq!(res, ContractError::PairConfigInvalidFeeBps {}); + + let info = mock_info(owner, &[]); + let msg = ExecuteMsg::UpdatePairConfig { + config: pair_config.clone(), + }; + + let res = execute(deps.as_mut(), env.clone(), info, msg).unwrap(); + assert_eq!(0, res.messages.len()); + + // It worked, let's query the state + let query_res = query(deps.as_ref(), env.clone(), QueryMsg::Config {}).unwrap(); + let config_res: ConfigResponse = from_json(&query_res).unwrap(); + assert_eq!(vec![pair_config.clone()], config_res.pair_configs); + + // Add second config + let pair_config_custom = PairConfig { + code_id: 100, + pair_type: PairType::Custom("test".to_string()), + total_fee_bps: 10, + maker_fee_bps: 20, + is_disabled: false, + is_generator_disabled: false, + permissioned: false, + }; + + let info = mock_info(owner, &[]); + let msg = ExecuteMsg::UpdatePairConfig { + config: pair_config_custom.clone(), + }; + + execute(deps.as_mut(), env.clone(), info, msg).unwrap(); + + // It worked, let's query the state + let query_res = query(deps.as_ref(), env.clone(), QueryMsg::Config {}).unwrap(); + let config_res: ConfigResponse = from_json(&query_res).unwrap(); + assert_eq!( + vec![pair_config_custom.clone(), pair_config.clone()], + config_res.pair_configs + ); +} + +#[test] +fn create_pair() { + let mut deps = mock_dependencies(&[]); + + let pair_config = PairConfig { + code_id: 321u64, + pair_type: PairType::Xyk {}, + total_fee_bps: 100, + maker_fee_bps: 10, + is_disabled: false, + is_generator_disabled: false, + permissioned: false, + }; + + let msg = InstantiateMsg { + pair_configs: vec![pair_config.clone()], + token_code_id: 123u64, + fee_address: None, + owner: "owner0000".to_string(), + generator_address: Some(String::from("generator")), + whitelist_code_id: 234u64, + coin_registry_address: "coin_registry".to_string(), + }; + + let env = mock_env(); + let info = mock_info("addr0000", &[]); + + // We can just call .unwrap() to assert this was a success + let _res = instantiate(deps.as_mut(), env, info, msg.clone()).unwrap(); + + let asset_infos = vec![ + AssetInfo::Token { + contract_addr: Addr::unchecked("asset0000"), + }, + AssetInfo::Token { + contract_addr: Addr::unchecked("asset0001"), + }, + ]; + + let config = CONFIG.load(&deps.storage); + let env = mock_env(); + let info = mock_info("addr0000", &[]); + + // Check pair creation using a non-whitelisted pair ID + let res = execute( + deps.as_mut(), + env.clone(), + info.clone(), + ExecuteMsg::CreatePair { + pair_type: PairType::Stable {}, + asset_infos: asset_infos.clone(), + init_params: None, + }, + ) + .unwrap_err(); + assert_eq!(res, ContractError::PairConfigNotFound {}); + + let res = execute( + deps.as_mut(), + env, + info, + ExecuteMsg::CreatePair { + pair_type: PairType::Xyk {}, + asset_infos: asset_infos.clone(), + init_params: None, + }, + ) + .unwrap(); + + assert_eq!( + res.attributes, + vec![ + attr("action", "create_pair"), + attr("pair", "asset0000-asset0001") + ] + ); + assert_eq!( + res.messages, + vec![SubMsg { + msg: WasmMsg::Instantiate { + msg: to_json_binary(&PairInstantiateMsg { + factory_addr: String::from(MOCK_CONTRACT_ADDR), + asset_infos: asset_infos.clone(), + token_code_id: msg.token_code_id, + init_params: None + }) + .unwrap(), + code_id: pair_config.code_id, + funds: vec![], + admin: Some(config.unwrap().owner.to_string()), + label: String::from("Palomadex pair"), + } + .into(), + id: 1, + gas_limit: None, + reply_on: ReplyOn::Success + }] + ); +} + +#[test] +fn register() { + let mut deps = mock_dependencies(&[]); + let owner = "owner0000"; + + let msg = InstantiateMsg { + pair_configs: vec![PairConfig { + code_id: 123u64, + pair_type: PairType::Xyk {}, + total_fee_bps: 100, + maker_fee_bps: 10, + is_disabled: false, + is_generator_disabled: false, + permissioned: false, + }], + token_code_id: 123u64, + fee_address: None, + generator_address: Some(String::from("generator")), + owner: owner.to_string(), + whitelist_code_id: 234u64, + coin_registry_address: "coin_registry".to_string(), + }; + + let env = mock_env(); + let info = mock_info("addr0000", &[]); + let _res = instantiate(deps.as_mut(), env, info, msg).unwrap(); + + let asset_infos = vec![ + AssetInfo::Token { + contract_addr: Addr::unchecked("asset0000"), + }, + AssetInfo::Token { + contract_addr: Addr::unchecked("asset0001"), + }, + ]; + + let msg = ExecuteMsg::CreatePair { + pair_type: PairType::Xyk {}, + asset_infos: asset_infos.clone(), + init_params: None, + }; + + let env = mock_env(); + let info = mock_info("addr0000", &[]); + let _res = execute(deps.as_mut(), env.clone(), info, msg).unwrap(); + + let pair0_addr = "pair0000".to_string(); + let pair0_info = PairInfo { + asset_infos: asset_infos.clone(), + contract_addr: Addr::unchecked("pair0000"), + liquidity_token: Addr::unchecked("liquidity0000"), + pair_type: PairType::Xyk {}, + }; + + let mut deployed_pairs = vec![(&pair0_addr, &pair0_info)]; + + // Register an Palomadex pair querier + deps.querier.with_palomadex_pairs(&deployed_pairs); + + let instantiate_reply = MsgInstantiateContractResponse { + contract_address: String::from("pair0000"), + data: vec![], + }; + + let mut encoded_instantiate_reply = Vec::::with_capacity(instantiate_reply.encoded_len()); + instantiate_reply + .encode(&mut encoded_instantiate_reply) + .unwrap(); + + let reply_msg = Reply { + id: 1, + result: SubMsgResult::Ok(SubMsgResponse { + events: vec![], + data: Some(encoded_instantiate_reply.into()), + }), + }; + + let _res = reply(deps.as_mut(), mock_env(), reply_msg.clone()).unwrap(); + + let query_res = query( + deps.as_ref(), + env.clone(), + QueryMsg::Pair { + asset_infos: asset_infos.clone(), + }, + ) + .unwrap(); + + let pair_res: PairInfo = from_json(&query_res).unwrap(); + assert_eq!( + pair_res, + PairInfo { + liquidity_token: Addr::unchecked("liquidity0000"), + contract_addr: Addr::unchecked("pair0000"), + asset_infos: asset_infos.clone(), + pair_type: PairType::Xyk {}, + } + ); + + // Check pair was registered + let res = reply(deps.as_mut(), mock_env(), reply_msg).unwrap_err(); + assert_eq!(res, ContractError::PairWasRegistered {}); + + // Store one more item to test query pairs + let asset_infos_2 = vec![ + AssetInfo::Token { + contract_addr: Addr::unchecked("asset0000"), + }, + AssetInfo::Token { + contract_addr: Addr::unchecked("asset0002"), + }, + ]; + + let msg = ExecuteMsg::CreatePair { + pair_type: PairType::Xyk {}, + asset_infos: asset_infos_2.clone(), + init_params: None, + }; + + let env = mock_env(); + let info = mock_info("addr0000", &[]); + let _res = execute(deps.as_mut(), env.clone(), info, msg).unwrap(); + + let pair1_addr = "pair0001".to_string(); + let pair1_info = PairInfo { + asset_infos: asset_infos_2.clone(), + contract_addr: Addr::unchecked("pair0001"), + liquidity_token: Addr::unchecked("liquidity0001"), + pair_type: PairType::Xyk {}, + }; + + deployed_pairs.push((&pair1_addr, &pair1_info)); + + // Register palomadex pair querier + deps.querier.with_palomadex_pairs(&deployed_pairs); + + let instantiate_reply = MsgInstantiateContractResponse { + contract_address: String::from("pair0001"), + data: vec![], + }; + + let mut encoded_instantiate_reply = Vec::::with_capacity(instantiate_reply.encoded_len()); + instantiate_reply + .encode(&mut encoded_instantiate_reply) + .unwrap(); + + let reply_msg = Reply { + id: 1, + result: SubMsgResult::Ok(SubMsgResponse { + events: vec![], + data: Some(encoded_instantiate_reply.into()), + }), + }; + + let _res = reply(deps.as_mut(), mock_env(), reply_msg.clone()).unwrap(); + + let query_msg = QueryMsg::Pairs { + start_after: None, + limit: None, + }; + + let res = query(deps.as_ref(), env.clone(), query_msg).unwrap(); + let pairs_res: PairsResponse = from_json(&res).unwrap(); + assert_eq!( + pairs_res.pairs, + vec![ + PairInfo { + liquidity_token: Addr::unchecked("liquidity0000"), + contract_addr: Addr::unchecked("pair0000"), + asset_infos: asset_infos.clone(), + pair_type: PairType::Xyk {}, + }, + PairInfo { + liquidity_token: Addr::unchecked("liquidity0001"), + contract_addr: Addr::unchecked("pair0001"), + asset_infos: asset_infos_2.clone(), + pair_type: PairType::Xyk {}, + } + ] + ); + + let query_msg = QueryMsg::Pairs { + start_after: None, + limit: Some(1), + }; + + let res = query(deps.as_ref(), env.clone(), query_msg).unwrap(); + let pairs_res: PairsResponse = from_json(&res).unwrap(); + assert_eq!( + pairs_res.pairs, + vec![PairInfo { + liquidity_token: Addr::unchecked("liquidity0000"), + contract_addr: Addr::unchecked("pair0000"), + asset_infos: asset_infos.clone(), + pair_type: PairType::Xyk {}, + }] + ); + + let query_msg = QueryMsg::Pairs { + start_after: Some(asset_infos.clone()), + limit: None, + }; + + let res = query(deps.as_ref(), env.clone(), query_msg).unwrap(); + let pairs_res: PairsResponse = from_json(&res).unwrap(); + assert_eq!( + pairs_res.pairs, + vec![PairInfo { + liquidity_token: Addr::unchecked("liquidity0001"), + contract_addr: Addr::unchecked("pair0001"), + asset_infos: asset_infos_2.clone(), + pair_type: PairType::Xyk {}, + }] + ); + + // Deregister from wrong acc + let env = mock_env(); + let info = mock_info("wrong_addr0000", &[]); + let res = execute( + deps.as_mut(), + env.clone(), + info, + ExecuteMsg::Deregister { + asset_infos: asset_infos_2.clone(), + }, + ) + .unwrap_err(); + + assert_eq!(res, ContractError::Unauthorized {}); + + // Proper deregister + let env = mock_env(); + let info = mock_info(owner, &[]); + let res = execute( + deps.as_mut(), + env.clone(), + info, + ExecuteMsg::Deregister { + asset_infos: asset_infos_2.clone(), + }, + ) + .unwrap(); + + assert_eq!(res.attributes[0], attr("action", "deregister")); + + let query_msg = QueryMsg::Pairs { + start_after: None, + limit: None, + }; + + let res = query(deps.as_ref(), env.clone(), query_msg).unwrap(); + let pairs_res: PairsResponse = from_json(&res).unwrap(); + assert_eq!( + pairs_res.pairs, + vec![PairInfo { + liquidity_token: Addr::unchecked("liquidity0000"), + contract_addr: Addr::unchecked("pair0000"), + asset_infos: asset_infos.clone(), + pair_type: PairType::Xyk {}, + },] + ); +} diff --git a/contracts/factory/tests/factory_helper.rs b/contracts/factory/tests/factory_helper.rs new file mode 100644 index 0000000..e1b2485 --- /dev/null +++ b/contracts/factory/tests/factory_helper.rs @@ -0,0 +1,171 @@ +#![cfg(not(tarpaulin_include))] + +use anyhow::Result as AnyResult; +use cosmwasm_std::{Addr, Binary}; +use cw20::MinterResponse; +use cw_multi_test::{App, AppResponse, ContractWrapper, Executor}; +use palomadex::asset::AssetInfo; +use palomadex::factory::{PairConfig, PairType}; + +pub struct FactoryHelper { + // pub owner: Addr, + pub factory: Addr, + pub cw20_token_code_id: u64, +} + +impl FactoryHelper { + pub fn init(router: &mut App, owner: &Addr) -> Self { + let astro_token_contract = Box::new(ContractWrapper::new_with_empty( + cw20_base::contract::execute, + cw20_base::contract::instantiate, + cw20_base::contract::query, + )); + + let cw20_token_code_id = router.store_code(astro_token_contract); + + let pair_contract = Box::new( + ContractWrapper::new_with_empty( + palomadex_pair::contract::execute, + palomadex_pair::contract::instantiate, + palomadex_pair::contract::query, + ) + .with_reply_empty(palomadex_pair::contract::reply), + ); + + let pair_code_id = router.store_code(pair_contract); + + let factory_contract = Box::new( + ContractWrapper::new_with_empty( + palomadex_factory::contract::execute, + palomadex_factory::contract::instantiate, + palomadex_factory::contract::query, + ) + .with_reply_empty(palomadex_factory::contract::reply), + ); + + let factory_code_id = router.store_code(factory_contract); + + let msg = palomadex::factory::InstantiateMsg { + pair_configs: vec![ + PairConfig { + code_id: pair_code_id, + pair_type: PairType::Xyk {}, + total_fee_bps: 100, + maker_fee_bps: 10, + is_disabled: false, + is_generator_disabled: false, + permissioned: false, + }, + PairConfig { + code_id: pair_code_id, + pair_type: PairType::Custom("transmuter".to_string()), + total_fee_bps: 0, + maker_fee_bps: 0, + is_disabled: false, + is_generator_disabled: false, + permissioned: true, + }, + ], + token_code_id: cw20_token_code_id, + fee_address: None, + generator_address: None, + owner: owner.to_string(), + whitelist_code_id: 0, + coin_registry_address: "coin_registry".to_string(), + }; + + let factory = router + .instantiate_contract( + factory_code_id, + owner.clone(), + &msg, + &[], + String::from("ASTRO"), + None, + ) + .unwrap(); + + Self { + // owner: owner.clone(), + factory, + cw20_token_code_id, + } + } + + pub fn update_config( + &mut self, + router: &mut App, + sender: &Addr, + token_code_id: Option, + fee_address: Option, + generator_address: Option, + whitelist_code_id: Option, + coin_registry_address: Option, + ) -> AnyResult { + let msg = palomadex::factory::ExecuteMsg::UpdateConfig { + token_code_id, + fee_address, + generator_address, + whitelist_code_id, + coin_registry_address, + }; + + router.execute_contract(sender.clone(), self.factory.clone(), &msg, &[]) + } + + pub fn create_pair( + &mut self, + router: &mut App, + sender: &Addr, + pair_type: PairType, + tokens: [&Addr; 2], + init_params: Option, + ) -> AnyResult { + let asset_infos = vec![ + AssetInfo::Token { + contract_addr: tokens[0].clone(), + }, + AssetInfo::Token { + contract_addr: tokens[1].clone(), + }, + ]; + + let msg = palomadex::factory::ExecuteMsg::CreatePair { + pair_type, + asset_infos, + init_params, + }; + + router.execute_contract(sender.clone(), self.factory.clone(), &msg, &[]) + } +} + +pub fn instantiate_token( + app: &mut App, + token_code_id: u64, + owner: &Addr, + token_name: &str, + decimals: Option, +) -> Addr { + let init_msg = palomadex::token::InstantiateMsg { + name: token_name.to_string(), + symbol: token_name.to_string(), + decimals: decimals.unwrap_or(6), + initial_balances: vec![], + mint: Some(MinterResponse { + minter: owner.to_string(), + cap: None, + }), + marketing: None, + }; + + app.instantiate_contract( + token_code_id, + owner.clone(), + &init_msg, + &[], + token_name, + None, + ) + .unwrap() +} diff --git a/contracts/factory/tests/integration.rs b/contracts/factory/tests/integration.rs new file mode 100644 index 0000000..2ae48a0 --- /dev/null +++ b/contracts/factory/tests/integration.rs @@ -0,0 +1,393 @@ +#![cfg(not(tarpaulin_include))] + +mod factory_helper; + +use cosmwasm_std::{attr, Addr}; + +use palomadex::asset::{AssetInfo, PairInfo}; +use palomadex::factory::{ + ConfigResponse, ExecuteMsg, FeeInfoResponse, InstantiateMsg, PairConfig, PairType, QueryMsg, +}; + +use crate::factory_helper::{instantiate_token, FactoryHelper}; +use cw_multi_test::{App, ContractWrapper, Executor}; +use palomadex_factory::error::ContractError; + +fn mock_app() -> App { + App::default() +} + +fn store_factory_code(app: &mut App) -> u64 { + let factory_contract = Box::new( + ContractWrapper::new_with_empty( + palomadex_factory::contract::execute, + palomadex_factory::contract::instantiate, + palomadex_factory::contract::query, + ) + .with_reply_empty(palomadex_factory::contract::reply), + ); + + app.store_code(factory_contract) +} + +#[test] +fn proper_initialization() { + let mut app = mock_app(); + + let owner = Addr::unchecked("owner"); + + let factory_code_id = store_factory_code(&mut app); + + let pair_configs = vec![PairConfig { + code_id: 321, + pair_type: PairType::Xyk {}, + total_fee_bps: 100, + maker_fee_bps: 10, + is_disabled: false, + is_generator_disabled: false, + permissioned: false, + }]; + + let msg = InstantiateMsg { + pair_configs: pair_configs.clone(), + token_code_id: 123, + fee_address: None, + owner: owner.to_string(), + generator_address: Some(String::from("generator")), + whitelist_code_id: 234u64, + coin_registry_address: "coin_registry".to_string(), + }; + + let factory_instance = app + .instantiate_contract( + factory_code_id, + Addr::unchecked(owner.clone()), + &msg, + &[], + "factory", + None, + ) + .unwrap(); + + let msg = QueryMsg::Config {}; + let config_res: ConfigResponse = app + .wrap() + .query_wasm_smart(&factory_instance, &msg) + .unwrap(); + + assert_eq!(123, config_res.token_code_id); + assert_eq!(pair_configs, config_res.pair_configs); + assert_eq!(owner, config_res.owner); +} + +#[test] +fn update_config() { + let mut app = mock_app(); + let owner = Addr::unchecked("owner"); + let mut helper = FactoryHelper::init(&mut app, &owner); + + // Update config + helper + .update_config( + &mut app, + &owner, + Some(200u64), + Some("fee".to_string()), + Some("generator".to_string()), + None, + None, + ) + .unwrap(); + + let config_res: ConfigResponse = app + .wrap() + .query_wasm_smart(&helper.factory, &QueryMsg::Config {}) + .unwrap(); + + assert_eq!(200u64, config_res.token_code_id); + assert_eq!("fee", config_res.fee_address.unwrap().to_string()); + assert_eq!( + "generator", + config_res.generator_address.unwrap().to_string() + ); + + // Unauthorized err + let res = helper + .update_config( + &mut app, + &Addr::unchecked("not_owner"), + None, + None, + None, + None, + None, + ) + .unwrap_err(); + assert_eq!(res.root_cause().to_string(), "Unauthorized"); +} + +#[test] +fn test_create_pair() { + let mut app = mock_app(); + let owner = Addr::unchecked("owner"); + let mut helper = FactoryHelper::init(&mut app, &owner); + + let token1 = instantiate_token( + &mut app, + helper.cw20_token_code_id, + &owner, + "tokenX", + Some(18), + ); + let token2 = instantiate_token( + &mut app, + helper.cw20_token_code_id, + &owner, + "tokenY", + Some(18), + ); + + let err = helper + .create_pair(&mut app, &owner, PairType::Xyk {}, [&token1, &token1], None) + .unwrap_err(); + assert_eq!( + err.root_cause().to_string(), + "Doubling assets in asset infos" + ); + + let res = helper + .create_pair(&mut app, &owner, PairType::Xyk {}, [&token1, &token2], None) + .unwrap(); + + let err = helper + .create_pair(&mut app, &owner, PairType::Xyk {}, [&token1, &token2], None) + .unwrap_err(); + assert_eq!(err.root_cause().to_string(), "Pair was already created"); + + assert_eq!(res.events[1].attributes[1], attr("action", "create_pair")); + assert_eq!( + res.events[1].attributes[2], + attr("pair", format!("{}-{}", token1.as_str(), token2.as_str())) + ); + + let res: PairInfo = app + .wrap() + .query_wasm_smart( + helper.factory.clone(), + &QueryMsg::Pair { + asset_infos: vec![ + AssetInfo::Token { + contract_addr: token1.clone(), + }, + AssetInfo::Token { + contract_addr: token2.clone(), + }, + ], + }, + ) + .unwrap(); + + // In multitest, contract names are counted in the order in which contracts are created + assert_eq!("contract0", helper.factory.to_string()); + assert_eq!("contract3", res.contract_addr.to_string()); + assert_eq!("contract4", res.liquidity_token.to_string()); + + // Create disabled pair type + app.execute_contract( + owner.clone(), + helper.factory.clone(), + &ExecuteMsg::UpdatePairConfig { + config: PairConfig { + code_id: 0, + pair_type: PairType::Custom("Custom".to_string()), + total_fee_bps: 100, + maker_fee_bps: 40, + is_disabled: true, + is_generator_disabled: false, + permissioned: false, + }, + }, + &[], + ) + .unwrap(); + + let token3 = instantiate_token( + &mut app, + helper.cw20_token_code_id, + &owner, + "tokenY", + Some(18), + ); + + let err = helper + .create_pair( + &mut app, + &Addr::unchecked("someone"), + PairType::Custom("Custom".to_string()), + [&token1, &token3], + None, + ) + .unwrap_err(); + assert_eq!(err.root_cause().to_string(), "Pair config disabled"); + + // Query fee info + let fee_info: FeeInfoResponse = app + .wrap() + .query_wasm_smart( + &helper.factory, + &QueryMsg::FeeInfo { + pair_type: PairType::Custom("Custom".to_string()), + }, + ) + .unwrap(); + assert_eq!(100, fee_info.total_fee_bps); + assert_eq!(40, fee_info.maker_fee_bps); + + // query blacklisted pairs + let pair_types: Vec = app + .wrap() + .query_wasm_smart(&helper.factory, &QueryMsg::BlacklistedPairTypes {}) + .unwrap(); + assert_eq!(pair_types, vec![PairType::Custom("Custom".to_string())]); +} + +#[test] +fn check_update_owner() { + let mut app = mock_app(); + let owner = Addr::unchecked("owner"); + let helper = FactoryHelper::init(&mut app, &owner); + + let new_owner = String::from("new_owner"); + + // New owner + let msg = ExecuteMsg::ProposeNewOwner { + owner: new_owner.clone(), + expires_in: 100, // seconds + }; + + // Unauthed check + let err = app + .execute_contract( + Addr::unchecked("not_owner"), + helper.factory.clone(), + &msg, + &[], + ) + .unwrap_err(); + assert_eq!(err.root_cause().to_string(), "Generic error: Unauthorized"); + + // Claim before proposal + let err = app + .execute_contract( + Addr::unchecked(new_owner.clone()), + helper.factory.clone(), + &ExecuteMsg::ClaimOwnership {}, + &[], + ) + .unwrap_err(); + assert_eq!( + err.root_cause().to_string(), + "Generic error: Ownership proposal not found" + ); + + // Propose new owner + app.execute_contract(Addr::unchecked("owner"), helper.factory.clone(), &msg, &[]) + .unwrap(); + + // Claim from invalid addr + let err = app + .execute_contract( + Addr::unchecked("invalid_addr"), + helper.factory.clone(), + &ExecuteMsg::ClaimOwnership {}, + &[], + ) + .unwrap_err(); + assert_eq!(err.root_cause().to_string(), "Generic error: Unauthorized"); + + // Drop ownership proposal + let err = app + .execute_contract( + Addr::unchecked(new_owner.clone()), + helper.factory.clone(), + &ExecuteMsg::DropOwnershipProposal {}, + &[], + ) + .unwrap_err(); + // new_owner is not an owner yet + assert_eq!(err.root_cause().to_string(), "Generic error: Unauthorized"); + + app.execute_contract( + owner.clone(), + helper.factory.clone(), + &ExecuteMsg::DropOwnershipProposal {}, + &[], + ) + .unwrap(); + + // Try to claim ownership + let err = app + .execute_contract( + Addr::unchecked(new_owner.clone()), + helper.factory.clone(), + &ExecuteMsg::ClaimOwnership {}, + &[], + ) + .unwrap_err(); + assert_eq!( + err.root_cause().to_string(), + "Generic error: Ownership proposal not found" + ); + + // Propose new owner again + app.execute_contract(Addr::unchecked("owner"), helper.factory.clone(), &msg, &[]) + .unwrap(); + // Claim ownership + app.execute_contract( + Addr::unchecked(new_owner.clone()), + helper.factory.clone(), + &ExecuteMsg::ClaimOwnership {}, + &[], + ) + .unwrap(); + + // Let's query the contract state + let msg = QueryMsg::Config {}; + let res: ConfigResponse = app.wrap().query_wasm_smart(&helper.factory, &msg).unwrap(); + + assert_eq!(res.owner, new_owner) +} + +#[test] +fn test_create_permissioned_pair() { + let mut app = mock_app(); + let owner = Addr::unchecked("owner"); + let mut helper = FactoryHelper::init(&mut app, &owner); + + let token1 = instantiate_token(&mut app, helper.cw20_token_code_id, &owner, "tokenX", None); + let token2 = instantiate_token(&mut app, helper.cw20_token_code_id, &owner, "tokenY", None); + + let err = helper + .create_pair( + &mut app, + &Addr::unchecked("random_stranger"), + PairType::Custom("transmuter".to_string()), + [&token1, &token2], + None, + ) + .unwrap_err(); + assert_eq!( + err.downcast::().unwrap(), + ContractError::Unauthorized {} + ); + + helper + .create_pair( + &mut app, + &owner, + PairType::Custom("transmuter".to_string()), + [&token1, &token2], + None, + ) + .unwrap(); +} diff --git a/contracts/pair/.cargo/config.toml b/contracts/pair/.cargo/config.toml new file mode 100644 index 0000000..23622ad --- /dev/null +++ b/contracts/pair/.cargo/config.toml @@ -0,0 +1,6 @@ +[alias] +wasm = "build --release --target wasm32-unknown-unknown" +wasm-debug = "build --target wasm32-unknown-unknown" +unit-test = "test --lib" +integration-test = "test --test integration" +schema = "run --example factory_schema" diff --git a/contracts/pair/Cargo.toml b/contracts/pair/Cargo.toml index 03962de..102434c 100644 --- a/contracts/pair/Cargo.toml +++ b/contracts/pair/Cargo.toml @@ -17,7 +17,6 @@ library = [] cosmwasm-schema.workspace = true cosmwasm-std.workspace = true cw-storage-plus.workspace = true -cw2.workspace = true cw20.workspace = true cw-utils.workspace = true schemars.workspace = true @@ -26,6 +25,6 @@ thiserror.workspace = true palomadex.workspace = true [dev-dependencies] -cw-multi-test = "2.0.0" +cw-multi-test = "1.2.0" proptest = "1.0" prost = "0.13.3" diff --git a/contracts/pair/examples/pair_schema.rs b/contracts/pair/examples/pair_schema.rs new file mode 100644 index 0000000..5ce162f --- /dev/null +++ b/contracts/pair/examples/pair_schema.rs @@ -0,0 +1,10 @@ +use cosmwasm_schema::write_api; +use palomadex::pair::{ExecuteMsg, InstantiateMsg, QueryMsg}; + +fn main() { + write_api! { + instantiate: InstantiateMsg, + query: QueryMsg, + execute: ExecuteMsg, + } +} diff --git a/contracts/pair/src/contract.rs b/contracts/pair/src/contract.rs index 93ac07c..5fb091c 100644 --- a/contracts/pair/src/contract.rs +++ b/contracts/pair/src/contract.rs @@ -11,6 +11,7 @@ use cosmwasm_std::{ }; use cw20::{Cw20ExecuteMsg, Cw20ReceiveMsg, MinterResponse}; +use cw_utils::parse_instantiate_response_data; use palomadex::asset::{ addr_opt_validate, check_swap_parameters, format_lp_token_name, Asset, AssetInfo, CoinsExt, PairInfo, MINIMUM_LIQUIDITY_AMOUNT, @@ -21,12 +22,11 @@ use palomadex::pair::{ DEFAULT_SLIPPAGE, MAX_ALLOWED_SLIPPAGE, MAX_FEE_SHARE_BPS, }; use palomadex::pair::{ - CumulativePricesResponse, Cw20HookMsg, ExecuteMsg, InstantiateMsg, PoolResponse, - QueryMsg, ReverseSimulationResponse, SimulationResponse, TWAP_PRECISION, + CumulativePricesResponse, Cw20HookMsg, ExecuteMsg, InstantiateMsg, PoolResponse, QueryMsg, + ReverseSimulationResponse, SimulationResponse, TWAP_PRECISION, }; use palomadex::querier::{query_factory_config, query_fee_info, query_supply}; use palomadex::{token::InstantiateMsg as TokenInstantiateMsg, U256}; -use cw_utils::parse_instantiate_response_data; use crate::error::ContractError; use crate::state::{Config, BALANCES, CONFIG}; @@ -108,7 +108,6 @@ pub fn instantiate( id: INSTANTIATE_TOKEN_REPLY_ID, gas_limit: None, reply_on: ReplyOn::Success, - payload: Binary::default(), }]; Ok(Response::new().add_submessages(sub_msg).add_attribute( @@ -132,8 +131,6 @@ pub fn reply(deps: DepsMut, _env: Env, msg: Reply) -> Result { let mut config: Config = CONFIG.load(deps.storage)?; @@ -162,7 +159,7 @@ pub fn reply(deps: DepsMut, _env: Env, msg: Reply) -> Result provide_liquidity( - deps, - env, - info, - assets, - slippage_tolerance, - receiver, - ), + } => provide_liquidity(deps, env, info, assets, slippage_tolerance, receiver), ExecuteMsg::Swap { offer_asset, belief_price, @@ -223,7 +213,6 @@ pub fn execute( ) } ExecuteMsg::UpdateConfig { params } => update_config(deps, env, info, params), - _ => Err(ContractError::NonSupported {}), } } @@ -292,13 +281,13 @@ pub fn receive_cw20( /// * **assets** is an array with assets available in the pool. /// /// * **slippage_tolerance** is an optional parameter which is used to specify how much -/// the pool price can move until the provide liquidity transaction goes through. +/// the pool price can move until the provide liquidity transaction goes through. /// /// * **auto_stake** is an optional parameter which determines whether the LP tokens minted after -/// liquidity provision are automatically staked in the Generator contract on behalf of the LP token receiver. +/// liquidity provision are automatically staked in the Generator contract on behalf of the LP token receiver. /// /// * **receiver** is an optional parameter which defines the receiver of the LP tokens. -/// If no custom receiver is specified, the pair will mint LP tokens for the function caller. +/// If no custom receiver is specified, the pair will mint LP tokens for the function caller. /// /// NOTE - the address that wants to provide liquidity should approve the pair contract to pull its relevant tokens. pub fn provide_liquidity( @@ -398,11 +387,7 @@ pub fn provide_liquidity( // Mint LP tokens for the sender or for the receiver (if set) let receiver = addr_opt_validate(deps.api, &receiver)?.unwrap_or_else(|| info.sender.clone()); - messages.extend(mint_liquidity_token_message( - &config, - &receiver, - share, - )?); + messages.extend(mint_liquidity_token_message(&config, &receiver, share)?); if config.track_asset_balances { for (i, pool) in pools.iter().enumerate() { @@ -441,7 +426,7 @@ pub fn provide_liquidity( /// * **amount** is the amount of LP tokens that will be minted for the recipient. /// /// * **auto_stake** determines whether the newly minted LP tokens will -/// be automatically staked in the Generator on behalf of the recipient. +/// be automatically staked in the Generator on behalf of the recipient. fn mint_liquidity_token_message( config: &Config, recipient: &Addr, @@ -546,7 +531,9 @@ pub fn get_share_in_assets(pools: &[Asset], amount: Uint128, total_share: Uint12 .iter() .map(|a| Asset { info: a.info.clone(), - amount: a.amount.multiply_ratio(share_ratio.numerator(), share_ratio.denominator()), + amount: a + .amount + .multiply_ratio(share_ratio.numerator(), share_ratio.denominator()), }) .collect() } @@ -651,7 +638,8 @@ pub fn swap( if let Some(fee_share) = config.fee_share.clone() { // Calculate the fee share amount from the full commission amount let share_fee_rate = Decimal::from_ratio(fee_share.bps, 10000u16); - fee_share_amount = fees_commission_amount.multiply_ratio(share_fee_rate.numerator(), share_fee_rate.denominator()); + fee_share_amount = fees_commission_amount + .multiply_ratio(share_fee_rate.numerator(), share_fee_rate.denominator()); if !fee_share_amount.is_zero() { // Subtract the fee share amount from the commission @@ -863,7 +851,10 @@ pub fn calculate_maker_fee( commission_amount: Uint128, maker_commission_rate: Decimal, ) -> Option { - let maker_fee: Uint128 = commission_amount.multiply_ratio(maker_commission_rate.numerator(), maker_commission_rate.denominator()); + let maker_fee: Uint128 = commission_amount.multiply_ratio( + maker_commission_rate.numerator(), + maker_commission_rate.denominator(), + ); if maker_fee.is_zero() { return None; } @@ -880,23 +871,23 @@ pub fn calculate_maker_fee( /// * **QueryMsg::Pair {}** Returns information about the pair in an object of type [`PairInfo`]. /// /// * **QueryMsg::Pool {}** Returns information about the amount of assets in the pair contract as -/// well as the amount of LP tokens issued using an object of type [`PoolResponse`]. +/// well as the amount of LP tokens issued using an object of type [`PoolResponse`]. /// /// * **QueryMsg::Share { amount }** Returns the amount of assets that could be withdrawn from the pool -/// using a specific amount of LP tokens. The result is returned in a vector that contains objects of type [`Asset`]. +/// using a specific amount of LP tokens. The result is returned in a vector that contains objects of type [`Asset`]. /// /// * **QueryMsg::Simulation { offer_asset }** Returns the result of a swap simulation using a [`SimulationResponse`] object. /// /// * **QueryMsg::ReverseSimulation { ask_asset }** Returns the result of a reverse swap simulation using -/// a [`ReverseSimulationResponse`] object. +/// a [`ReverseSimulationResponse`] object. /// /// * **QueryMsg::CumulativePrices {}** Returns information about cumulative prices for the assets in the -/// pool using a [`CumulativePricesResponse`] object. +/// pool using a [`CumulativePricesResponse`] object. /// /// * **QueryMsg::Config {}** Returns the configuration for the pair contract using a [`ConfigResponse`] object. /// /// * **QueryMsg::AssetBalanceAt { asset_info, block_height }** Returns the balance of the specified asset that was in the pool -/// just preceeding the moment of the specified block height creation. +/// just preceeding the moment of the specified block height creation. #[cfg_attr(not(feature = "library"), entry_point)] pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { match msg { @@ -993,7 +984,7 @@ pub fn query_simulation(deps: Deps, offer_asset: Asset) -> StdResult Env { #[test] fn compute_swap_rounding() { let offer_pool = Uint128::from(5_000_000_000_000_u128); - let ask_pool = Uint128::from(1_000_000_000_u128); - let return_amount = Uint128::from(0_u128); + let ask_pool = Uint128::from(5_000_000_000_000_u128); + let return_amount = Uint128::from(100_u128); let spread_amount = Uint128::from(0_u128); let commission_amount = Uint128::from(0_u128); - let offer_amount = Uint128::from(1_u128); + let offer_amount = Uint128::from(100_u128); assert_eq!( compute_swap(offer_pool, ask_pool, offer_amount, Decimal::zero()), diff --git a/contracts/pair_stable/.cargo/config.toml b/contracts/pair_stable/.cargo/config.toml new file mode 100644 index 0000000..e6dbd73 --- /dev/null +++ b/contracts/pair_stable/.cargo/config.toml @@ -0,0 +1,6 @@ +[alias] +wasm = "build --release --target wasm32-unknown-unknown" +wasm-debug = "build --target wasm32-unknown-unknown" +unit-test = "test --lib" +integration-test = "test --test integration" +schema = "run --example pair_stable_schema" diff --git a/contracts/pair_stable/Cargo.toml b/contracts/pair_stable/Cargo.toml new file mode 100644 index 0000000..1240f35 --- /dev/null +++ b/contracts/pair_stable/Cargo.toml @@ -0,0 +1,42 @@ +[package] +name = "palomadex-pair-stable" +version = "0.1.0" +authors = ["Volume Finance"] +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +# use library feature to disable all instantiate/execute/query exports +library = [] + +[lints.rust] +unexpected_cfgs = { level = "warn", check-cfg = ["cfg(tarpaulin_include)"] } + +[dependencies] +cosmwasm-schema.workspace = true +cosmwasm-std.workspace = true +cw-storage-plus.workspace = true +cw20.workspace = true +cw-utils.workspace = true +schemars.workspace = true +serde.workspace = true +thiserror.workspace = true +palomadex.workspace = true +itertools.workspace = true +palomadex-circular-buffer = { path = "../../packages/circular_buffer" } + +[dev-dependencies] +anyhow = "1.0" +cw-multi-test = "1.2.0" +proptest = "1.0" +prost = "0.13.3" +cw20-base = "1.1.2" +palomadex-factory = { path = "../factory" } +palomadex-native-coin-registry = { path = "../../packages/native_coin_registry" } +basic-stableswap-sim = { path = "../../packages/basic_stableswap_sim"} +derivative = "2.2" +palomadex-mocks ={ path= "../../packages/palomadex_mocks" } \ No newline at end of file diff --git a/contracts/pair_stable/examples/pair_stable_schema.rs b/contracts/pair_stable/examples/pair_stable_schema.rs new file mode 100644 index 0000000..5ce162f --- /dev/null +++ b/contracts/pair_stable/examples/pair_stable_schema.rs @@ -0,0 +1,10 @@ +use cosmwasm_schema::write_api; +use palomadex::pair::{ExecuteMsg, InstantiateMsg, QueryMsg}; + +fn main() { + write_api! { + instantiate: InstantiateMsg, + query: QueryMsg, + execute: ExecuteMsg, + } +} diff --git a/contracts/pair_stable/proptest-regressions/testing.txt b/contracts/pair_stable/proptest-regressions/testing.txt new file mode 100644 index 0000000..fcb3d0a --- /dev/null +++ b/contracts/pair_stable/proptest-regressions/testing.txt @@ -0,0 +1,9 @@ +# Seeds for failure cases proptest has generated in the past. It is +# automatically read and these particular cases re-run before any +# novel cases are generated. +# +# It is recommended to check this file in to source control so that +# everyone who runs the test benefits from these saved cases. +cc ff664d4c7e174ac1187f380a6318272a734d84d774b5cbfb5cd7edf395cc16df # shrinks to balance_in = 21633764903, balance_out = 21633764904, amount_in = 1000, amp = 2 +cc b0dba5468968f8f2c0b12a9d9eebcf705c01bfd1078d314c514fc51255a2b13f # shrinks to balance_in = 134252243642038913, balance_out = 970750304795919205, amount_in = 1000, amp = 137 +cc d0548269c64d3e8847cfbe6f3bba5c7cf3ff4ff428389529eb212e6135b14971 # shrinks to balance_in = 2466798567, balance_out = 11879561542795017, amount_in = 1000, amp = 1 diff --git a/contracts/pair_stable/src/contract.rs b/contracts/pair_stable/src/contract.rs new file mode 100644 index 0000000..45f733f --- /dev/null +++ b/contracts/pair_stable/src/contract.rs @@ -0,0 +1,1215 @@ +use std::collections::HashMap; +use std::str::FromStr; +use std::vec; + +#[cfg(not(feature = "library"))] +use cosmwasm_std::entry_point; +use cosmwasm_std::{ + attr, from_json, to_json_binary, wasm_execute, wasm_instantiate, Addr, Binary, CosmosMsg, + Decimal, Decimal256, Deps, DepsMut, Env, Fraction, MessageInfo, QuerierWrapper, Reply, + Response, StdError, StdResult, SubMsg, SubMsgResponse, SubMsgResult, Uint128, WasmMsg, +}; +use cw20::{Cw20ExecuteMsg, Cw20ReceiveMsg, MinterResponse}; +use cw_utils::parse_instantiate_response_data; +use itertools::Itertools; + +use palomadex::asset::{ + addr_opt_validate, check_swap_parameters, format_lp_token_name, Asset, AssetInfo, CoinsExt, + Decimal256Ext, DecimalAsset, PairInfo, MINIMUM_LIQUIDITY_AMOUNT, +}; + +use palomadex::cosmwasm_ext::IntegerToDecimal; +use palomadex::factory::PairType; +use palomadex::pair::{ + ConfigResponse, CumulativePricesResponse, FeeShareConfig, InstantiateMsg, StablePoolParams, + StablePoolUpdateParams, DEFAULT_SLIPPAGE, MAX_ALLOWED_SLIPPAGE, MAX_FEE_SHARE_BPS, + MIN_TRADE_SIZE, +}; + +use palomadex::observation::{query_observation, PrecommitObservation, OBSERVATIONS_SIZE}; +use palomadex::pair::{ + Cw20HookMsg, ExecuteMsg, PoolResponse, QueryMsg, ReverseSimulationResponse, SimulationResponse, + StablePoolConfig, +}; +use palomadex::querier::{query_factory_config, query_fee_info, query_supply}; +use palomadex::token::InstantiateMsg as TokenInstantiateMsg; +use palomadex::DecimalCheckedOps; +use palomadex_circular_buffer::BufferManager; + +use crate::error::ContractError; +use crate::math::{ + calc_y, compute_d, AMP_PRECISION, MAX_AMP, MAX_AMP_CHANGE, MIN_AMP_CHANGING_TIME, +}; +use crate::state::{get_precision, store_precisions, Config, CONFIG, OBSERVATIONS}; +use crate::utils::{ + accumulate_prices, accumulate_swap_sizes, adjust_precision, check_asset_infos, check_assets, + check_cw20_in_pool, compute_current_amp, compute_swap, determine_base_quote_amount, + get_share_in_assets, mint_liquidity_token_message, select_pools, SwapResult, +}; + +/// A `reply` call code ID of sub-message. +const INSTANTIATE_TOKEN_REPLY_ID: u64 = 1; +/// Number of assets in the pool. +const N_COINS: usize = 2; + +/// Creates a new contract with the specified parameters in [`InstantiateMsg`]. +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn instantiate( + mut deps: DepsMut, + env: Env, + _info: MessageInfo, + msg: InstantiateMsg, +) -> Result { + check_asset_infos(deps.api, &msg.asset_infos)?; + + if msg.asset_infos.len() != N_COINS { + return Err(ContractError::InvalidNumberOfAssets(N_COINS)); + } + + if msg.init_params.is_none() { + return Err(ContractError::InitParamsNotFound {}); + } + + let params: StablePoolParams = from_json(msg.init_params.unwrap())?; + + if params.amp == 0 || params.amp > MAX_AMP { + return Err(ContractError::IncorrectAmp {}); + } + + let factory_addr = deps.api.addr_validate(&msg.factory_addr)?; + let greatest_precision = store_precisions(deps.branch(), &msg.asset_infos, &factory_addr)?; + + // Initializing cumulative prices + let mut cumulative_prices = vec![]; + for from_pool in &msg.asset_infos { + for to_pool in &msg.asset_infos { + if !from_pool.eq(to_pool) { + cumulative_prices.push((from_pool.clone(), to_pool.clone(), Uint128::zero())) + } + } + } + + let config = Config { + owner: addr_opt_validate(deps.api, ¶ms.owner)?, + pair_info: PairInfo { + contract_addr: env.contract.address.clone(), + liquidity_token: Addr::unchecked(""), + asset_infos: msg.asset_infos.clone(), + pair_type: PairType::Stable {}, + }, + factory_addr, + block_time_last: 0, + init_amp: params.amp * AMP_PRECISION, + init_amp_time: env.block.time.seconds(), + next_amp: params.amp * AMP_PRECISION, + next_amp_time: env.block.time.seconds(), + greatest_precision, + cumulative_prices, + fee_share: None, + }; + + CONFIG.save(deps.storage, &config)?; + BufferManager::init(deps.storage, OBSERVATIONS, OBSERVATIONS_SIZE)?; + + let token_name = format_lp_token_name(&msg.asset_infos, &deps.querier)?; + + // Create LP token + let sub_msg = SubMsg::reply_on_success( + wasm_instantiate( + msg.token_code_id, + &TokenInstantiateMsg { + name: token_name, + symbol: "uLP".to_string(), + decimals: greatest_precision, + initial_balances: vec![], + mint: Some(MinterResponse { + minter: env.contract.address.to_string(), + cap: None, + }), + marketing: None, + }, + vec![], + String::from("Palomadex LP token"), + )?, + INSTANTIATE_TOKEN_REPLY_ID, + ); + + Ok(Response::new().add_submessage(sub_msg)) +} + +/// The entry point to the contract for processing replies from submessages. +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn reply(deps: DepsMut, _env: Env, msg: Reply) -> Result { + match msg { + Reply { + id: INSTANTIATE_TOKEN_REPLY_ID, + result: + SubMsgResult::Ok(SubMsgResponse { + data: Some(data), .. + }), + } => { + let mut config = CONFIG.load(deps.storage)?; + + if config.pair_info.liquidity_token != Addr::unchecked("") { + return Err(ContractError::Unauthorized {}); + } + + let init_response = parse_instantiate_response_data(data.as_slice()) + .map_err(|e| StdError::generic_err(format!("{e}")))?; + config.pair_info.liquidity_token = + deps.api.addr_validate(&init_response.contract_address)?; + CONFIG.save(deps.storage, &config)?; + Ok(Response::new() + .add_attribute("liquidity_token_addr", config.pair_info.liquidity_token)) + } + _ => Err(ContractError::FailedToParseReply {}), + } +} + +/// Exposes all the execute functions available in the contract. +/// +/// ## Variants +/// * **ExecuteMsg::UpdateConfig { params: Binary }** Updates the contract configuration with the specified +/// input parameters. +/// +/// * **ExecuteMsg::Receive(msg)** Receives a message of type [`Cw20ReceiveMsg`] and processes +/// it depending on the received template. +/// +/// * **ExecuteMsg::ProvideLiquidity { +/// assets, +/// slippage_tolerance, +/// auto_stake, +/// receiver, +/// }** Provides liquidity in the pair using the specified input parameters. +/// +/// * **ExecuteMsg::Swap { +/// offer_asset, +/// belief_price, +/// max_spread, +/// to, +/// }** Performs an swap using the specified parameters. +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn execute( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: ExecuteMsg, +) -> Result { + match msg { + ExecuteMsg::UpdateConfig { params } => update_config(deps, env, info, params), + ExecuteMsg::Receive(msg) => receive_cw20(deps, env, info, msg), + ExecuteMsg::ProvideLiquidity { + assets, receiver, .. + } => provide_liquidity(deps, env, info, assets, receiver), + ExecuteMsg::Swap { + offer_asset, + ask_asset_info, + belief_price, + max_spread, + to, + .. + } => { + offer_asset.info.check(deps.api)?; + if !offer_asset.is_native_token() { + return Err(ContractError::Cw20DirectSwap {}); + } + offer_asset.assert_sent_native_token_balance(&info)?; + + let to_addr = addr_opt_validate(deps.api, &to)?; + + swap( + deps, + env, + info.sender, + offer_asset, + ask_asset_info, + belief_price, + max_spread, + to_addr, + ) + } + } +} + +/// Receives a message of type [`Cw20ReceiveMsg`] and processes it depending on the received template. +/// +/// * **cw20_msg** is the CW20 receive message to process. +pub fn receive_cw20( + deps: DepsMut, + env: Env, + info: MessageInfo, + cw20_msg: Cw20ReceiveMsg, +) -> Result { + match from_json(&cw20_msg.msg)? { + Cw20HookMsg::Swap { + ask_asset_info, + belief_price, + max_spread, + to, + } => { + let config = CONFIG.load(deps.storage)?; + + // Only asset contract can execute this message + check_cw20_in_pool(&config, &info.sender)?; + + let to_addr = addr_opt_validate(deps.api, &to)?; + swap( + deps, + env, + Addr::unchecked(cw20_msg.sender), + Asset { + info: AssetInfo::Token { + contract_addr: info.sender, + }, + amount: cw20_msg.amount, + }, + ask_asset_info, + belief_price, + max_spread, + to_addr, + ) + } + Cw20HookMsg::WithdrawLiquidity { assets } => withdraw_liquidity( + deps, + env, + info, + Addr::unchecked(cw20_msg.sender), + cw20_msg.amount, + assets, + ), + } +} + +/// Provides liquidity with the specified input parameters. +/// +/// * **assets** vector with assets available in the pool. +/// +/// * **auto_stake** determines whether the resulting LP tokens are automatically staked in +/// the Generator contract to receive token incentives. +/// +/// * **receiver** address that receives LP tokens. If this address isn't specified, the function will default to the caller. +/// +/// NOTE - the address that wants to provide liquidity should approve the pair contract to pull its relevant tokens. +pub fn provide_liquidity( + deps: DepsMut, + env: Env, + info: MessageInfo, + assets: Vec, + receiver: Option, +) -> Result { + check_assets(deps.api, &assets)?; + + let mut config = CONFIG.load(deps.storage)?; + info.funds + .assert_coins_properly_sent(&assets, &config.pair_info.asset_infos)?; + + if assets.len() != config.pair_info.asset_infos.len() { + return Err(ContractError::InvalidNumberOfAssets( + config.pair_info.asset_infos.len(), + )); + } + + let pools: HashMap<_, _> = config + .pair_info + .query_pools(&deps.querier, &env.contract.address)? + .into_iter() + .map(|pool| (pool.info, pool.amount)) + .collect(); + + let mut non_zero_flag = false; + + let mut assets_collection = assets + .clone() + .into_iter() + .map(|asset| { + // Check that at least one asset is non-zero + if !asset.amount.is_zero() { + non_zero_flag = true; + } + + // Get appropriate pool + let pool = pools + .get(&asset.info) + .copied() + .ok_or_else(|| ContractError::InvalidAsset(asset.info.to_string()))?; + + Ok((asset, pool)) + }) + .collect::, ContractError>>()?; + + // If some assets are omitted then add them explicitly with 0 deposit + pools.iter().for_each(|(pool_info, pool_amount)| { + if !assets.iter().any(|asset| asset.info.eq(pool_info)) { + assets_collection.push(( + Asset { + amount: Uint128::zero(), + info: pool_info.clone(), + }, + *pool_amount, + )); + } + }); + + if !non_zero_flag { + return Err(ContractError::InvalidZeroAmount {}); + } + + let mut messages = vec![]; + for (deposit, pool) in assets_collection.iter_mut() { + // We cannot put a zero amount into an empty pool. + if deposit.amount.is_zero() && pool.is_zero() { + return Err(ContractError::InvalidProvideLPsWithSingleToken {}); + } + + // Transfer only non-zero amount + if !deposit.amount.is_zero() { + // If the pool is a token contract, then we need to execute a TransferFrom msg to receive funds + if let AssetInfo::Token { contract_addr } = &deposit.info { + messages.push(CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: contract_addr.to_string(), + msg: to_json_binary(&Cw20ExecuteMsg::TransferFrom { + owner: info.sender.to_string(), + recipient: env.contract.address.to_string(), + amount: deposit.amount, + })?, + funds: vec![], + })) + } else { + // If the asset is a native token, the pool balance already increased + // To calculate the pool balance properly, we should subtract the user deposit from the recorded pool token amount + *pool = pool.checked_sub(deposit.amount)?; + } + } + } + + let assets_collection = assets_collection + .iter() + .cloned() + .map(|(asset, pool)| { + let coin_precision = get_precision(deps.storage, &asset.info)?; + Ok(( + asset.to_decimal_asset(coin_precision)?, + Decimal256::with_precision(pool, coin_precision)?, + )) + }) + .collect::>>()?; + + let amp = compute_current_amp(&config, &env)?; + + // Invariant (D) after deposit added + let new_balances = assets_collection + .iter() + .map(|(deposit, pool)| Ok(pool + deposit.amount)) + .collect::>>()?; + let deposit_d = compute_d(amp, &new_balances)?; + + let total_share = query_supply(&deps.querier, &config.pair_info.liquidity_token)?; + let share = if total_share.is_zero() { + let share = deposit_d + .to_uint128_with_precision(config.greatest_precision)? + .checked_sub(MINIMUM_LIQUIDITY_AMOUNT) + .map_err(|_| ContractError::MinimumLiquidityAmountError {})?; + + // share cannot become zero after minimum liquidity subtraction + if share.is_zero() { + return Err(ContractError::MinimumLiquidityAmountError {}); + } + + messages.extend(mint_liquidity_token_message( + &config, + &env.contract.address, + MINIMUM_LIQUIDITY_AMOUNT, + )?); + + share + } else { + // Initial invariant (D) + let old_balances = assets_collection + .iter() + .map(|(_, pool)| *pool) + .collect_vec(); + let init_d = compute_d(amp, &old_balances)?; + + let share = Decimal256::with_precision(total_share, config.greatest_precision)? + .checked_multiply_ratio(deposit_d.saturating_sub(init_d), init_d)? + .to_uint128_with_precision(config.greatest_precision)?; + + if share.is_zero() { + return Err(ContractError::LiquidityAmountTooSmall {}); + } + + share + }; + + // Mint LP token for the caller (or for the receiver if it was set) + let receiver = addr_opt_validate(deps.api, &receiver)?.unwrap_or_else(|| info.sender.clone()); + messages.extend(mint_liquidity_token_message(&config, &receiver, share)?); + + let pools = pools + .into_iter() + .map(|(info, amount)| { + let precision = get_precision(deps.storage, &info)?; + Ok(DecimalAsset { + info, + amount: Decimal256::with_precision(amount, precision)?, + }) + }) + .collect::>>()?; + + if accumulate_prices(deps.storage, &env, &mut config, &pools)? { + CONFIG.save(deps.storage, &config)?; + } + + Ok(Response::new().add_messages(messages).add_attributes(vec![ + attr("action", "provide_liquidity"), + attr("sender", info.sender), + attr("receiver", receiver), + attr("assets", assets.iter().join(", ")), + attr("share", share), + ])) +} + +/// Withdraw liquidity from the pool. +/// * **sender** is the address that will receive assets back from the pair contract. +/// +/// * **amount** is the amount of LP tokens to burn. +pub fn withdraw_liquidity( + deps: DepsMut, + env: Env, + info: MessageInfo, + sender: Addr, + amount: Uint128, + assets: Vec, +) -> Result { + let mut config = CONFIG.load(deps.storage)?; + + if info.sender != config.pair_info.liquidity_token { + return Err(ContractError::Unauthorized {}); + } + + let (pools, total_share) = pool_info(deps.querier, &config)?; + + let refund_assets = if assets.is_empty() { + // Usual withdraw (balanced) + get_share_in_assets(&pools, amount, total_share) + } else { + return Err(StdError::generic_err("Imbalanced withdraw is currently disabled").into()); + }; + + let mut messages = refund_assets + .clone() + .into_iter() + .map(|asset| asset.into_msg(&sender)) + .collect::>>()?; + messages.push( + wasm_execute( + &config.pair_info.liquidity_token, + &Cw20ExecuteMsg::Burn { amount }, + vec![], + )? + .into(), + ); + + let pools = pools + .iter() + .map(|pool| { + let precision = get_precision(deps.storage, &pool.info)?; + pool.to_decimal_asset(precision) + }) + .collect::>>()?; + + if accumulate_prices(deps.storage, &env, &mut config, &pools)? { + CONFIG.save(deps.storage, &config)?; + } + + Ok(Response::new().add_messages(messages).add_attributes(vec![ + attr("action", "withdraw_liquidity"), + attr("sender", sender), + attr("withdrawn_share", amount), + attr("refund_assets", refund_assets.iter().join(", ")), + ])) +} + +/// Performs an swap operation with the specified parameters. +/// +/// * **sender** is the sender of the swap operation. +/// +/// * **offer_asset** proposed asset for swapping. +/// +/// * **belief_price** is used to calculate the maximum swap spread. +/// +/// * **max_spread** sets the maximum spread of the swap operation. +/// +/// * **to** sets the recipient of the swap operation. +/// +/// NOTE - the address that wants to swap should approve the pair contract to pull the offer token. +#[allow(clippy::too_many_arguments)] +pub fn swap( + deps: DepsMut, + env: Env, + sender: Addr, + offer_asset: Asset, + ask_asset_info: Option, + belief_price: Option, + max_spread: Option, + to: Option, +) -> Result { + let mut config = CONFIG.load(deps.storage)?; + + // If the asset balance already increased + // We should subtract the user deposit from the pool offer asset amount + let pools = config + .pair_info + .query_pools(&deps.querier, &env.contract.address)? + .into_iter() + .map(|mut pool| { + if pool.info.equal(&offer_asset.info) { + pool.amount = pool.amount.checked_sub(offer_asset.amount)?; + } + let token_precision = get_precision(deps.storage, &pool.info)?; + Ok(DecimalAsset { + info: pool.info, + amount: Decimal256::with_precision(pool.amount, token_precision)?, + }) + }) + .collect::>>()?; + + let (offer_pool, ask_pool) = + select_pools(Some(&offer_asset.info), ask_asset_info.as_ref(), &pools)?; + + let offer_precision = get_precision(deps.storage, &offer_pool.info)?; + + // Check if the liquidity is non-zero + check_swap_parameters( + pools + .iter() + .map(|pool| { + pool.amount + .to_uint128_with_precision(get_precision(deps.storage, &pool.info)?) + }) + .collect::>>()?, + offer_asset.amount, + )?; + + let offer_asset_dec = offer_asset.to_decimal_asset(offer_precision)?; + + let SwapResult { + return_amount, + spread_amount, + } = compute_swap( + deps.storage, + &env, + &config, + &offer_asset_dec, + &offer_pool, + &ask_pool, + &pools, + )?; + + // Get fee info from the factory + let fee_info = query_fee_info( + &deps.querier, + &config.factory_addr, + config.pair_info.pair_type.clone(), + )?; + let commission_amount = fee_info.total_fee_rate.checked_mul_uint128(return_amount)?; + let return_amount = return_amount.saturating_sub(commission_amount); + + // Check the max spread limit (if it was specified) + assert_max_spread( + belief_price, + max_spread, + offer_asset.amount, + return_amount + commission_amount, + spread_amount, + )?; + + let receiver = to.unwrap_or_else(|| sender.clone()); + + let return_asset = Asset { + info: ask_pool.info.clone(), + amount: return_amount, + }; + + let mut messages = vec![]; + if !return_amount.is_zero() { + messages.push(return_asset.into_msg(receiver.clone())?) + } + + // If this pool is configured to share fees, calculate the amount to send + // to the receiver and add the transfer message + // The calculation works as follows: We take the share percentage first, + // and the remainder is then split between LPs and maker + let mut fees_commission_amount = commission_amount; + let mut fee_share_amount = Uint128::zero(); + if let Some(ref fee_share) = config.fee_share { + // Calculate the fee share amount from the full commission amount + let share_fee_rate = Decimal::from_ratio(fee_share.bps, 10000u16); + fee_share_amount = fees_commission_amount * share_fee_rate; + + if !fee_share_amount.is_zero() { + // Subtract the fee share amount from the commission + fees_commission_amount = fees_commission_amount.saturating_sub(fee_share_amount); + + // Build send message for the shared amount + let fee_share_msg = Asset { + info: ask_pool.info.clone(), + amount: fee_share_amount, + } + .into_msg(&fee_share.recipient)?; + messages.push(fee_share_msg); + } + } + + // Compute the Maker fee + let mut maker_fee_amount = Uint128::zero(); + if let Some(fee_address) = fee_info.fee_address { + if let Some(f) = calculate_maker_fee( + &ask_pool.info, + fees_commission_amount, + fee_info.maker_fee_rate, + ) { + maker_fee_amount = f.amount; + messages.push(f.into_msg(fee_address)?); + } + } + + if accumulate_prices(deps.storage, &env, &mut config, &pools)? { + CONFIG.save(deps.storage, &config)?; + } + + // Store observation from precommit data + accumulate_swap_sizes(deps.storage, &env)?; + + // Store time series data in precommit observation. + // Skipping small unsafe values which can seriously mess oracle price due to rounding errors. + // This data will be reflected in observations on the next action. + let ask_precision = get_precision(deps.storage, &ask_pool.info)?; + if offer_asset_dec.amount >= MIN_TRADE_SIZE + && return_amount.to_decimal256(ask_precision)? >= MIN_TRADE_SIZE + { + // Store time series data + let (base_amount, quote_amount) = + determine_base_quote_amount(&pools, &offer_asset, return_amount)?; + PrecommitObservation::save(deps.storage, &env, base_amount, quote_amount)?; + } + + Ok(Response::new() + .add_messages( + // 1. send collateral tokens from the contract to a user + // 2. send inactive commission fees to the Maker contract + messages, + ) + .add_attributes(vec![ + attr("action", "swap"), + attr("sender", sender), + attr("receiver", receiver), + attr("offer_asset", offer_asset.info.to_string()), + attr("ask_asset", ask_pool.info.to_string()), + attr("offer_amount", offer_asset.amount), + attr("return_amount", return_amount), + attr("spread_amount", spread_amount), + attr("commission_amount", commission_amount), + attr("maker_fee_amount", maker_fee_amount), + attr("fee_share_amount", fee_share_amount), + ])) +} + +/// Calculates the amount of fees the Maker contract gets according to specified pair parameters. +/// Returns a [`None`] if the Maker fee is zero, otherwise returns a [`Asset`] struct with the specified attributes. +/// +/// * **pool_info** contains information about the pool asset for which the commission will be calculated. +/// +/// * **commission_amount** is the total amount of fees charged for a swap. +/// +/// * **maker_commission_rate** is the percentage of fees that go to the Maker contract. +pub fn calculate_maker_fee( + pool_info: &AssetInfo, + commission_amount: Uint128, + maker_commission_rate: Decimal, +) -> Option { + let maker_fee: Uint128 = commission_amount * maker_commission_rate; + if maker_fee.is_zero() { + return None; + } + + Some(Asset { + info: pool_info.clone(), + amount: maker_fee, + }) +} + +/// Exposes all the queries available in the contract. +/// ## Queries +/// * **QueryMsg::Pair {}** Returns information about the pair in an object of type [`PairInfo`]. +/// +/// * **QueryMsg::Pool {}** Returns information about the amount of assets in the pair contract as +/// well as the amount of LP tokens issued using an object of type [`PoolResponse`]. +/// +/// * **QueryMsg::Share { amount }** Returns the amount of assets that could be withdrawn from the pool +/// using a specific amount of LP tokens. The result is returned in a vector that contains objects of type [`Asset`]. +/// +/// * **QueryMsg::Simulation { offer_asset }** Returns the result of a swap simulation using a [`SimulationResponse`] object. +/// +/// * **QueryMsg::ReverseSimulation { ask_asset }** Returns the result of a reverse swap simulation using +/// a [`ReverseSimulationResponse`] object. +/// +/// * **QueryMsg::CumulativePrices {}** Returns information about cumulative prices for the assets in the +/// pool using a [`CumulativePricesResponse`] object. +/// +/// * **QueryMsg::Config {}** Returns the configuration for the pair contract using a [`ConfigResponse`] object. +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { + match msg { + QueryMsg::Pair {} => to_json_binary(&CONFIG.load(deps.storage)?.pair_info), + QueryMsg::Pool {} => to_json_binary(&query_pool(deps)?), + QueryMsg::Share { amount } => to_json_binary(&query_share(deps, amount)?), + QueryMsg::Simulation { + offer_asset, + ask_asset_info, + } => to_json_binary(&query_simulation(deps, env, offer_asset, ask_asset_info)?), + QueryMsg::ReverseSimulation { + offer_asset_info, + ask_asset, + } => to_json_binary(&query_reverse_simulation( + deps, + env, + ask_asset, + offer_asset_info, + )?), + QueryMsg::CumulativePrices {} => to_json_binary(&query_cumulative_prices(deps, env)?), + QueryMsg::Observe { seconds_ago } => { + to_json_binary(&query_observation(deps, env, OBSERVATIONS, seconds_ago)?) + } + QueryMsg::Config {} => to_json_binary(&query_config(deps, env)?), + QueryMsg::QueryComputeD {} => to_json_binary(&query_compute_d(deps, env)?), + _ => Err(StdError::generic_err("Query is not supported")), + } +} + +/// Returns the amounts of assets in the pair contract as well as the amount of LP +/// tokens currently minted in an object of type [`PoolResponse`]. +pub fn query_pool(deps: Deps) -> StdResult { + let config = CONFIG.load(deps.storage)?; + let (assets, total_share) = pool_info(deps.querier, &config)?; + + let resp = PoolResponse { + assets, + total_share, + }; + + Ok(resp) +} + +/// Returns the amount of assets that could be withdrawn from the pool using a specific amount of LP tokens. +/// The result is returned in a vector that contains objects of type [`Asset`]. +/// +/// * **amount** is the amount of LP tokens for which we calculate associated amounts of assets. +pub fn query_share(deps: Deps, amount: Uint128) -> StdResult> { + let config = CONFIG.load(deps.storage)?; + let (pools, total_share) = pool_info(deps.querier, &config)?; + let refund_assets = get_share_in_assets(&pools, amount, total_share); + + Ok(refund_assets) +} + +/// Returns information about a swap simulation in a [`SimulationResponse`] object. +/// +/// * **offer_asset** is the asset to swap as well as an amount of the said asset. +pub fn query_simulation( + deps: Deps, + env: Env, + offer_asset: Asset, + ask_asset_info: Option, +) -> StdResult { + let config = CONFIG.load(deps.storage)?; + let pools = config.pair_info.query_pools_decimal( + &deps.querier, + &config.pair_info.contract_addr, + &config.factory_addr, + )?; + + let (offer_pool, ask_pool) = + select_pools(Some(&offer_asset.info), ask_asset_info.as_ref(), &pools) + .map_err(|err| StdError::generic_err(format!("{err}")))?; + + let offer_precision = get_precision(deps.storage, &offer_pool.info)?; + + if check_swap_parameters( + pools + .iter() + .map(|pool| { + pool.amount + .to_uint128_with_precision(get_precision(deps.storage, &pool.info)?) + }) + .collect::>>()?, + offer_asset.amount, + ) + .is_err() + { + return Ok(SimulationResponse { + return_amount: Uint128::zero(), + spread_amount: Uint128::zero(), + commission_amount: Uint128::zero(), + }); + } + + let SwapResult { + return_amount, + spread_amount, + } = compute_swap( + deps.storage, + &env, + &config, + &offer_asset.to_decimal_asset(offer_precision)?, + &offer_pool, + &ask_pool, + &pools, + ) + .map_err(|err| StdError::generic_err(format!("{err}")))?; + + // Get fee info from factory + let fee_info = query_fee_info( + &deps.querier, + &config.factory_addr, + config.pair_info.pair_type.clone(), + )?; + + let commission_amount = fee_info.total_fee_rate.checked_mul_uint128(return_amount)?; + let return_amount = return_amount.saturating_sub(commission_amount); + + Ok(SimulationResponse { + return_amount, + spread_amount, + commission_amount, + }) +} + +/// Returns information about a reverse swap simulation in a [`ReverseSimulationResponse`] object. +/// +/// * **ask_asset** is the asset to swap to as well as the desired amount of ask +/// assets to receive from the swap. +/// +/// * **offer_asset_info** is optional field which specifies the asset to swap from. +/// May be omitted only in case the pool length is 2. +pub fn query_reverse_simulation( + deps: Deps, + env: Env, + ask_asset: Asset, + offer_asset_info: Option, +) -> StdResult { + let config = CONFIG.load(deps.storage)?; + let pools = config.pair_info.query_pools_decimal( + &deps.querier, + &config.pair_info.contract_addr, + &config.factory_addr, + )?; + let (offer_pool, ask_pool) = + select_pools(offer_asset_info.as_ref(), Some(&ask_asset.info), &pools) + .map_err(|err| StdError::generic_err(format!("{err}")))?; + + let offer_precision = get_precision(deps.storage, &offer_pool.info)?; + let ask_precision = get_precision(deps.storage, &ask_asset.info)?; + + // Check the swap parameters are valid + if check_swap_parameters( + pools + .iter() + .map(|pool| { + pool.amount + .to_uint128_with_precision(get_precision(deps.storage, &pool.info)?) + }) + .collect::>>()?, + ask_asset.amount, + ) + .is_err() + { + return Ok(ReverseSimulationResponse { + offer_amount: Uint128::zero(), + spread_amount: Uint128::zero(), + commission_amount: Uint128::zero(), + }); + } + + // Get fee info from the factory + let fee_info = query_fee_info( + &deps.querier, + &config.factory_addr, + config.pair_info.pair_type.clone(), + )?; + let before_commission = (Decimal256::one() + - Decimal256::new(fee_info.total_fee_rate.atomics().into())) + .inv() + .ok_or_else(|| StdError::generic_err("The pool must have less than 100% fee!"))? + .checked_mul(Decimal256::with_precision(ask_asset.amount, ask_precision)?)?; + + let xp = pools.into_iter().map(|pool| pool.amount).collect_vec(); + let new_offer_pool_amount = calc_y( + compute_current_amp(&config, &env)?, + ask_pool.amount - before_commission, + &xp, + config.greatest_precision, + )?; + + let offer_amount = new_offer_pool_amount.checked_sub( + offer_pool + .amount + .to_uint128_with_precision(config.greatest_precision)?, + )?; + let offer_amount = adjust_precision(offer_amount, config.greatest_precision, offer_precision)?; + + Ok(ReverseSimulationResponse { + offer_amount, + spread_amount: offer_amount + .saturating_sub(before_commission.to_uint128_with_precision(offer_precision)?), + commission_amount: fee_info + .total_fee_rate + .checked_mul_uint128(before_commission.to_uint128_with_precision(ask_precision)?)?, + }) +} + +/// Returns information about cumulative prices for the assets in the pool using a [`CumulativePricesResponse`] object. +pub fn query_cumulative_prices(deps: Deps, env: Env) -> StdResult { + let mut config = CONFIG.load(deps.storage)?; + let (assets, total_share) = pool_info(deps.querier, &config)?; + let decimal_assets = assets + .iter() + .cloned() + .map(|asset| { + let precision = get_precision(deps.storage, &asset.info)?; + asset.to_decimal_asset(precision) + }) + .collect::>>()?; + + accumulate_prices(deps.storage, &env, &mut config, &decimal_assets) + .map_err(|err| StdError::generic_err(format!("{err}")))?; + + Ok(CumulativePricesResponse { + assets, + total_share, + cumulative_prices: config.cumulative_prices, + }) +} + +/// Returns the pair contract configuration in a [`ConfigResponse`] object. +pub fn query_config(deps: Deps, env: Env) -> StdResult { + let config = CONFIG.load(deps.storage)?; + let factory_config = query_factory_config(&deps.querier, &config.factory_addr)?; + Ok(ConfigResponse { + block_time_last: config.block_time_last, + params: Some(to_json_binary(&StablePoolConfig { + amp: Decimal::from_ratio(compute_current_amp(&config, &env)?, AMP_PRECISION), + fee_share: config.fee_share, + })?), + owner: config.owner.unwrap_or(factory_config.owner), + factory_addr: config.factory_addr, + }) +} + +/// If `belief_price` and `max_spread` are both specified, we compute a new spread, +/// otherwise we just use the swap spread to check `max_spread`. +/// +/// * **belief_price** belief price used in the swap. +/// +/// * **max_spread** max spread allowed so that the swap can be executed successfully. +/// +/// * **offer_amount** amount of assets to swap. +/// +/// * **return_amount** amount of assets to receive from the swap. +/// +/// * **spread_amount** spread used in the swap. +pub fn assert_max_spread( + belief_price: Option, + max_spread: Option, + offer_amount: Uint128, + return_amount: Uint128, + spread_amount: Uint128, +) -> Result<(), ContractError> { + let default_spread = Decimal::from_str(DEFAULT_SLIPPAGE)?; + let max_allowed_spread = Decimal::from_str(MAX_ALLOWED_SLIPPAGE)?; + + let max_spread = max_spread.unwrap_or(default_spread); + if max_spread.gt(&max_allowed_spread) { + return Err(ContractError::AllowedSpreadAssertion {}); + } + + if let Some(belief_price) = belief_price { + let expected_return = offer_amount + * belief_price.inv().ok_or_else(|| { + ContractError::Std(StdError::generic_err( + "Invalid belief_price. Check the input values.", + )) + })?; + + let spread_amount = expected_return.saturating_sub(return_amount); + + if return_amount < expected_return + && Decimal::from_ratio(spread_amount, expected_return) > max_spread + { + return Err(ContractError::MaxSpreadAssertion {}); + } + } else if Decimal::from_ratio(spread_amount, return_amount + spread_amount) > max_spread { + return Err(ContractError::MaxSpreadAssertion {}); + } + + Ok(()) +} + +/// Returns the total amount of assets in the pool as well as the total amount of LP tokens currently minted. +pub fn pool_info(querier: QuerierWrapper, config: &Config) -> StdResult<(Vec, Uint128)> { + let pools = config + .pair_info + .query_pools(&querier, &config.pair_info.contract_addr)?; + let total_share = query_supply(&querier, &config.pair_info.liquidity_token)?; + + Ok((pools, total_share)) +} + +/// Updates the pool configuration with the specified parameters in the `params` variable. +/// +/// * **params** new parameter values. +pub fn update_config( + deps: DepsMut, + env: Env, + info: MessageInfo, + params: Binary, +) -> Result { + let mut config = CONFIG.load(deps.storage)?; + let factory_config = query_factory_config(&deps.querier, &config.factory_addr)?; + + if info.sender + != if let Some(ref owner) = config.owner { + owner.to_owned() + } else { + factory_config.owner + } + { + return Err(ContractError::Unauthorized {}); + } + + let mut response = Response::default(); + + match from_json::(¶ms)? { + StablePoolUpdateParams::StartChangingAmp { + next_amp, + next_amp_time, + } => start_changing_amp(config, deps, env, next_amp, next_amp_time)?, + StablePoolUpdateParams::StopChangingAmp {} => stop_changing_amp(config, deps, env)?, + StablePoolUpdateParams::EnableFeeShare { + fee_share_bps, + fee_share_address, + } => { + // Enable fee sharing for this contract + // If fee sharing is already enabled, we should be able to overwrite + // the values currently set + + // Ensure the fee share isn't 0 and doesn't exceed the maximum allowed value + if fee_share_bps == 0 || fee_share_bps > MAX_FEE_SHARE_BPS { + return Err(ContractError::FeeShareOutOfBounds {}); + } + + // Set sharing config + config.fee_share = Some(FeeShareConfig { + bps: fee_share_bps, + recipient: deps.api.addr_validate(&fee_share_address)?, + }); + + CONFIG.save(deps.storage, &config)?; + + response.attributes.push(attr("action", "enable_fee_share")); + response + .attributes + .push(attr("fee_share_bps", fee_share_bps.to_string())); + response + .attributes + .push(attr("fee_share_address", fee_share_address)); + } + StablePoolUpdateParams::DisableFeeShare => { + // Disable fee sharing for this contract by setting bps and + // address back to None + config.fee_share = None; + CONFIG.save(deps.storage, &config)?; + response + .attributes + .push(attr("action", "disable_fee_share")); + } + } + + Ok(response) +} + +/// Start changing the AMP value. +/// +/// * **next_amp** new value for AMP. +/// +/// * **next_amp_time** end time when the pool amplification will be equal to `next_amp`. +fn start_changing_amp( + mut config: Config, + deps: DepsMut, + env: Env, + next_amp: u64, + next_amp_time: u64, +) -> Result<(), ContractError> { + if next_amp == 0 || next_amp > MAX_AMP { + return Err(ContractError::IncorrectAmp {}); + } + + let current_amp = compute_current_amp(&config, &env)?.u64(); + + let next_amp_with_precision = next_amp * AMP_PRECISION; + + if next_amp_with_precision * MAX_AMP_CHANGE < current_amp + || next_amp_with_precision > current_amp * MAX_AMP_CHANGE + { + return Err(ContractError::MaxAmpChangeAssertion {}); + } + + let block_time = env.block.time.seconds(); + + if block_time < config.init_amp_time + MIN_AMP_CHANGING_TIME + || next_amp_time < block_time + MIN_AMP_CHANGING_TIME + { + return Err(ContractError::MinAmpChangingTimeAssertion {}); + } + + config.init_amp = current_amp; + config.next_amp = next_amp_with_precision; + config.init_amp_time = block_time; + config.next_amp_time = next_amp_time; + + CONFIG.save(deps.storage, &config)?; + + Ok(()) +} + +/// Stop changing the AMP value. +fn stop_changing_amp(mut config: Config, deps: DepsMut, env: Env) -> StdResult<()> { + let current_amp = compute_current_amp(&config, &env)?; + let block_time = env.block.time.seconds(); + + config.init_amp = current_amp.u64(); + config.next_amp = current_amp.u64(); + config.init_amp_time = block_time; + config.next_amp_time = block_time; + + // now (block_time < next_amp_time) is always False, so we return the saved AMP + CONFIG.save(deps.storage, &config)?; + + Ok(()) +} +/// Compute the current pool D value. +fn query_compute_d(deps: Deps, env: Env) -> StdResult { + let config = CONFIG.load(deps.storage)?; + + let amp = compute_current_amp(&config, &env)?; + let pools = config + .pair_info + .query_pools_decimal(&deps.querier, env.contract.address, &config.factory_addr)? + .into_iter() + .map(|pool| pool.amount) + .collect::>(); + + compute_d(amp, &pools) + .map_err(|_| StdError::generic_err("Failed to calculate the D"))? + .to_uint128_with_precision(config.greatest_precision) +} diff --git a/contracts/pair_stable/src/error.rs b/contracts/pair_stable/src/error.rs new file mode 100644 index 0000000..a3eae71 --- /dev/null +++ b/contracts/pair_stable/src/error.rs @@ -0,0 +1,110 @@ +use cosmwasm_std::{CheckedMultiplyRatioError, ConversionOverflowError, OverflowError, StdError}; +use thiserror::Error; + +use palomadex::{asset::MINIMUM_LIQUIDITY_AMOUNT, pair::MAX_FEE_SHARE_BPS}; +use palomadex_circular_buffer::error::BufferError; + +use crate::math::{MAX_AMP, MAX_AMP_CHANGE, MIN_AMP_CHANGING_TIME}; + +/// This enum describes stableswap pair contract errors +#[derive(Error, Debug, PartialEq)] +pub enum ContractError { + #[error("{0}")] + Std(#[from] StdError), + + #[error("{0}")] + CheckedMultiplyRatioError(#[from] CheckedMultiplyRatioError), + + #[error("{0}")] + CircularBuffer(#[from] BufferError), + + #[error("Unauthorized")] + Unauthorized {}, + + #[error("CW20 tokens can be swapped via Cw20::Send message only")] + Cw20DirectSwap {}, + + #[error("Doubling assets in asset infos")] + DoublingAssets {}, + + #[error("Event of zero transfer")] + InvalidZeroAmount {}, + + #[error("Insufficient amount of liquidity")] + LiquidityAmountTooSmall {}, + + #[error("Provided spread amount exceeds allowed limit")] + AllowedSpreadAssertion {}, + + #[error("Operation exceeds max spread limit")] + MaxSpreadAssertion {}, + + #[error("Native token balance mismatch between the argument and the transferred")] + AssetMismatch {}, + + #[error( + "Amp coefficient must be greater than 0 and less than or equal to {}", + MAX_AMP + )] + IncorrectAmp {}, + + #[error( + "The difference between the old and new amp value must not exceed {} times", + MAX_AMP_CHANGE + )] + MaxAmpChangeAssertion {}, + + #[error( + "Amp coefficient cannot be changed more often than once per {} seconds", + MIN_AMP_CHANGING_TIME + )] + MinAmpChangingTimeAssertion {}, + + #[error("You need to provide init params")] + InitParamsNotFound {}, + + #[error("Generator address is not set in factory. Cannot autostake")] + AutoStakeError {}, + + #[error("It is not possible to provide liquidity with one token for an empty pool")] + InvalidProvideLPsWithSingleToken {}, + + #[error("The asset {0} does not belong to the pair")] + InvalidAsset(String), + + #[error("Ask or offer asset is missed")] + VariableAssetMissed {}, + + #[error("Source and target assets are the same")] + SameAssets {}, + + #[error("Invalid number of assets. This pair support only {0} assets")] + InvalidNumberOfAssets(usize), + + #[error("Contract can't be migrated!")] + MigrationError {}, + + #[error("Initial liquidity must be more than {}", MINIMUM_LIQUIDITY_AMOUNT)] + MinimumLiquidityAmountError {}, + + #[error("Failed to parse or process reply message")] + FailedToParseReply {}, + + #[error( + "Fee share is 0 or exceeds maximum allowed value of {} bps", + MAX_FEE_SHARE_BPS + )] + FeeShareOutOfBounds {}, +} + +impl From for ContractError { + fn from(o: OverflowError) -> Self { + StdError::from(o).into() + } +} + +impl From for ContractError { + fn from(o: ConversionOverflowError) -> Self { + StdError::from(o).into() + } +} diff --git a/contracts/pair_stable/src/lib.rs b/contracts/pair_stable/src/lib.rs new file mode 100644 index 0000000..456a243 --- /dev/null +++ b/contracts/pair_stable/src/lib.rs @@ -0,0 +1,13 @@ +pub mod contract; +pub mod math; +pub mod state; + +pub mod error; + +pub mod utils; + +#[cfg(test)] +mod testing; + +#[cfg(test)] +mod mock_querier; diff --git a/contracts/pair_stable/src/math.rs b/contracts/pair_stable/src/math.rs new file mode 100644 index 0000000..f9fb91e --- /dev/null +++ b/contracts/pair_stable/src/math.rs @@ -0,0 +1,119 @@ +use cosmwasm_std::{Decimal256, StdError, StdResult, Uint128, Uint64}; + +use palomadex::asset::Decimal256Ext; + +/// The maximum number of calculation steps for Newton's method. +const ITERATIONS: u8 = 64; + +pub const MAX_AMP: u64 = 1_000_000; +pub const MAX_AMP_CHANGE: u64 = 10; +pub const MIN_AMP_CHANGING_TIME: u64 = 86400; +pub const AMP_PRECISION: u64 = 100; +/// N = 2 +pub const N_COINS: Decimal256 = Decimal256::raw(2000000000000000000); +/// 1e-6 +pub const TOL: Decimal256 = Decimal256::raw(1000000000000); + +/// Computes the stableswap invariant (D). +/// +/// * **Equation** +/// +/// A * sum(x_i) * n**n + D = A * D * n**n + D**(n+1) / (n**n * prod(x_i)) +/// +pub fn compute_d(amp: Uint64, pools: &[Decimal256]) -> StdResult { + let leverage = Decimal256::from_ratio(amp, AMP_PRECISION) * N_COINS; + let amount_a_times_coins = pools[0] * N_COINS; + let amount_b_times_coins = pools[1] * N_COINS; + + let sum_x = pools[0].checked_add(pools[1])?; // sum(x_i), a.k.a S + if sum_x.is_zero() { + Ok(Decimal256::zero()) + } else { + let mut d_previous: Decimal256; + let mut d: Decimal256 = sum_x; + + // Newton's method to approximate D + for _ in 0..ITERATIONS { + let d_product = d.pow(3) / (amount_a_times_coins * amount_b_times_coins); + d_previous = d; + d = calculate_step(d, leverage, sum_x, d_product)?; + // Equality with the precision of 1e-6 + if d.abs_diff(d_previous) <= TOL { + return Ok(d); + } + } + + Err(StdError::generic_err( + "Newton method for D failed to converge", + )) + } +} + +/// Helper function used to calculate the D invariant as a last step in the `compute_d` public function. +/// +/// * **Equation**: +/// +/// d = (leverage * sum_x + d_product * n_coins) * initial_d / ((leverage - 1) * initial_d + (n_coins + 1) * d_product) +fn calculate_step( + initial_d: Decimal256, + leverage: Decimal256, + sum_x: Decimal256, + d_product: Decimal256, +) -> StdResult { + let leverage_mul = leverage.checked_mul(sum_x)?; + let d_p_mul = d_product.checked_mul(N_COINS)?; + + let l_val = leverage_mul.checked_add(d_p_mul)?.checked_mul(initial_d)?; + + let leverage_sub = initial_d.checked_mul(leverage - Decimal256::one())?; + let n_coins_sum = d_product.checked_mul(N_COINS.checked_add(Decimal256::one())?)?; + + let r_val = leverage_sub.checked_add(n_coins_sum)?; + + l_val + .checked_div(r_val) + .map_err(|e| StdError::generic_err(e.to_string())) +} + +/// Compute the swap amount `y` in proportion to `x`. +/// +/// * **Solve for y** +/// +/// y**2 + y * (sum' - (A*n**n - 1) * D / (A * n**n)) = D ** (n + 1) / (n ** (2 * n) * prod' * A) +/// +/// y**2 + b*y = c +pub(crate) fn calc_y( + amp: Uint64, + new_amount: Decimal256, + xp: &[Decimal256], + target_precision: u8, +) -> StdResult { + let d = compute_d(amp, xp)?; + let leverage = Decimal256::from_ratio(amp, 1u8) * N_COINS; + let amp_prec = Decimal256::from_ratio(AMP_PRECISION, 1u8); + + let c = d.checked_pow(3)?.checked_mul(amp_prec)? + / new_amount + .checked_mul(N_COINS * N_COINS)? + .checked_mul(leverage)?; + + let b = new_amount.checked_add(d.checked_mul(amp_prec)? / leverage)?; + + // Solve for y by approximating: y**2 + b*y = c + let mut y_prev; + let mut y = d; + for _ in 0..ITERATIONS { + y_prev = y; + y = y + .checked_pow(2)? + .checked_add(c)? + .checked_div(y.checked_mul(N_COINS)?.checked_add(b)?.checked_sub(d)?) + .map_err(|e| StdError::generic_err(e.to_string()))?; + if y.abs_diff(y_prev) <= TOL { + return y.to_uint128_with_precision(target_precision); + } + } + + // Should definitely converge in 64 iterations. + Err(StdError::generic_err("y is not converging")) +} diff --git a/contracts/pair_stable/src/mock_querier.rs b/contracts/pair_stable/src/mock_querier.rs new file mode 100644 index 0000000..2816597 --- /dev/null +++ b/contracts/pair_stable/src/mock_querier.rs @@ -0,0 +1,206 @@ +use std::collections::HashMap; + +use cosmwasm_std::testing::{MockApi, MockQuerier, MockStorage, MOCK_CONTRACT_ADDR}; +use cosmwasm_std::{ + from_json, to_json_binary, Addr, Coin, Empty, OwnedDeps, Querier, QuerierResult, QueryRequest, + SystemError, SystemResult, Uint128, WasmQuery, +}; +use cw20::{BalanceResponse, Cw20QueryMsg, TokenInfoResponse}; + +use palomadex::factory::QueryMsg::{Config, FeeInfo}; +use palomadex::factory::{Config as FactoryConfig, ConfigResponse, FeeInfoResponse}; + +/// mock_dependencies is a drop-in replacement for cosmwasm_std::testing::mock_dependencies. +/// This uses the Palomadex CustomQuerier. +pub fn mock_dependencies( + contract_balance: &[Coin], +) -> OwnedDeps { + let custom_querier: WasmMockQuerier = + WasmMockQuerier::new(MockQuerier::new(&[(MOCK_CONTRACT_ADDR, contract_balance)])); + + OwnedDeps { + storage: MockStorage::default(), + api: MockApi::default(), + querier: custom_querier, + custom_query_type: Default::default(), + } +} + +pub struct WasmMockQuerier { + base: MockQuerier, + token_querier: TokenQuerier, +} + +#[derive(Clone, Default)] +pub struct TokenQuerier { + // This lets us iterate over all pairs that match the first string + balances: HashMap>, +} + +impl TokenQuerier { + pub fn new(balances: &[(&String, &[(&String, &Uint128)])]) -> Self { + TokenQuerier { + balances: balances_to_map(balances), + } + } +} + +pub(crate) fn balances_to_map( + balances: &[(&String, &[(&String, &Uint128)])], +) -> HashMap> { + let mut balances_map: HashMap> = HashMap::new(); + for (contract_addr, balances) in balances.iter() { + let mut contract_balances_map: HashMap = HashMap::new(); + for (addr, balance) in balances.iter() { + contract_balances_map.insert(addr.to_string(), **balance); + } + + balances_map.insert(contract_addr.to_string(), contract_balances_map); + } + balances_map +} + +impl Querier for WasmMockQuerier { + fn raw_query(&self, bin_request: &[u8]) -> QuerierResult { + // MockQuerier doesn't support Custom, so we ignore it completely + let request: QueryRequest = match from_json(bin_request) { + Ok(v) => v, + Err(e) => { + return SystemResult::Err(SystemError::InvalidRequest { + error: format!("Parsing query request: {}", e), + request: bin_request.into(), + }) + } + }; + self.handle_query(&request) + } +} + +impl WasmMockQuerier { + pub fn handle_query(&self, request: &QueryRequest) -> QuerierResult { + match &request { + QueryRequest::Wasm(WasmQuery::Smart { contract_addr, msg }) => { + if contract_addr == "factory" { + match from_json(&msg).unwrap() { + FeeInfo { .. } => SystemResult::Ok( + to_json_binary(&FeeInfoResponse { + fee_address: Some(Addr::unchecked("fee_address")), + total_fee_bps: 30, + maker_fee_bps: 1660, + }) + .into(), + ), + Config {} => SystemResult::Ok( + to_json_binary(&ConfigResponse { + owner: Addr::unchecked("owner"), + pair_configs: vec![], + token_code_id: 0, + fee_address: Some(Addr::unchecked("fee_address")), + generator_address: None, + whitelist_code_id: 0, + coin_registry_address: Addr::unchecked("coin_registry"), + }) + .into(), + ), + _ => panic!("DO NOT ENTER HERE"), + } + } else { + match from_json(&msg).unwrap() { + Cw20QueryMsg::TokenInfo {} => { + let balances: &HashMap = + match self.token_querier.balances.get(contract_addr) { + Some(balances) => balances, + None => { + return SystemResult::Err(SystemError::Unknown {}); + } + }; + + let mut total_supply = Uint128::zero(); + + for balance in balances { + total_supply += *balance.1; + } + + SystemResult::Ok( + to_json_binary(&TokenInfoResponse { + name: "mAPPL".to_string(), + symbol: "mAPPL".to_string(), + decimals: 6, + total_supply: total_supply, + }) + .into(), + ) + } + Cw20QueryMsg::Balance { address } => { + let balances: &HashMap = + match self.token_querier.balances.get(contract_addr) { + Some(balances) => balances, + None => { + return SystemResult::Err(SystemError::Unknown {}); + } + }; + + let balance = match balances.get(&address) { + Some(v) => v, + None => { + return SystemResult::Err(SystemError::Unknown {}); + } + }; + + SystemResult::Ok( + to_json_binary(&BalanceResponse { balance: *balance }).into(), + ) + } + _ => panic!("DO NOT ENTER HERE"), + } + } + } + QueryRequest::Wasm(WasmQuery::Raw { contract_addr, key }) => { + if contract_addr == "factory" { + if key.as_slice() == b"config".as_slice() { + SystemResult::Ok( + to_json_binary(&FactoryConfig { + owner: Addr::unchecked("owner"), + token_code_id: 0, + fee_address: Some(Addr::unchecked("fee_address")), + generator_address: None, + whitelist_code_id: 0, + coin_registry_address: Addr::unchecked("coin_registry"), + }) + .into(), + ) + } else if key.as_slice() == b"pairs_to_migrate".as_slice() { + SystemResult::Ok(to_json_binary(&Vec::::new()).into()) + } else { + panic!("DO NOT ENTER HERE"); + } + } else if contract_addr == "coin_registry" { + SystemResult::Ok(to_json_binary(&6).into()) + } else { + panic!("DO NOT ENTER HERE"); + } + } + _ => self.base.handle_query(request), + } + } +} + +impl WasmMockQuerier { + pub fn new(base: MockQuerier) -> Self { + WasmMockQuerier { + base, + token_querier: TokenQuerier::default(), + } + } + + // Configure the mint whitelist mock querier + pub fn with_token_balances(&mut self, balances: &[(&String, &[(&String, &Uint128)])]) { + self.token_querier = TokenQuerier::new(balances); + } + + pub fn with_balance(&mut self, balances: &[(&String, &[Coin])]) { + for (addr, balance) in balances { + self.base.update_balance(addr.to_string(), balance.to_vec()); + } + } +} diff --git a/contracts/pair_stable/src/state.rs b/contracts/pair_stable/src/state.rs new file mode 100644 index 0000000..b3cb586 --- /dev/null +++ b/contracts/pair_stable/src/state.rs @@ -0,0 +1,70 @@ +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{Addr, DepsMut, StdResult, Storage, Uint128}; +use cw_storage_plus::{Item, Map}; + +use palomadex::asset::{AssetInfo, PairInfo}; +use palomadex::common::OwnershipProposal; +use palomadex::observation::Observation; +use palomadex::pair::FeeShareConfig; +use palomadex_circular_buffer::CircularBuffer; + +/// This structure stores the main stableswap pair parameters. +#[cw_serde] +pub struct Config { + /// The contract owner + pub owner: Option, + /// The pair information stored in a [`PairInfo`] struct + pub pair_info: PairInfo, + /// The factory contract address + pub factory_addr: Addr, + /// The last timestamp when the pair contract update the asset cumulative prices + pub block_time_last: u64, + /// This is the current amplification used in the pool + pub init_amp: u64, + /// This is the start time when amplification starts to scale up or down + pub init_amp_time: u64, + /// This is the target amplification to reach at `next_amp_time` + pub next_amp: u64, + /// This is the timestamp when the current pool amplification should be `next_amp` + pub next_amp_time: u64, + /// The greatest precision of assets in the pool + pub greatest_precision: u8, + /// The vector contains cumulative prices for each pair of assets in the pool + pub cumulative_prices: Vec<(AssetInfo, AssetInfo, Uint128)>, + // The config for swap fee sharing + pub fee_share: Option, +} + +/// Circular buffer to store trade size observations +pub const OBSERVATIONS: CircularBuffer = + CircularBuffer::new("observations_state", "observations_buffer"); + +pub const CONFIG: Item = Item::new("config"); + +/// Stores map of AssetInfo (as String) -> precision +pub const PRECISIONS: Map = Map::new("precisions"); + +/// Stores the latest contract ownership transfer proposal +pub const OWNERSHIP_PROPOSAL: Item = Item::new("ownership_proposal"); + +/// Store all token precisions and return the greatest one. +pub(crate) fn store_precisions( + deps: DepsMut, + asset_infos: &[AssetInfo], + factory_addr: &Addr, +) -> StdResult { + let mut max = 0u8; + + for asset_info in asset_infos { + let precision = asset_info.decimals(&deps.querier, factory_addr)?; + max = max.max(precision); + PRECISIONS.save(deps.storage, asset_info.to_string(), &precision)?; + } + + Ok(max) +} + +/// Loads precision of the given asset info. +pub(crate) fn get_precision(storage: &dyn Storage, asset_info: &AssetInfo) -> StdResult { + PRECISIONS.load(storage, asset_info.to_string()) +} diff --git a/contracts/pair_stable/src/testing.rs b/contracts/pair_stable/src/testing.rs new file mode 100644 index 0000000..4108a60 --- /dev/null +++ b/contracts/pair_stable/src/testing.rs @@ -0,0 +1,1398 @@ +use std::error::Error; +use std::str::FromStr; + +use basic_stableswap_sim::StableSwapModel; +use cosmwasm_std::testing::{mock_env, mock_info, MOCK_CONTRACT_ADDR}; +use cosmwasm_std::{ + attr, coin, to_json_binary, Addr, BankMsg, BlockInfo, Coin, CosmosMsg, Decimal, DepsMut, Env, + Reply, ReplyOn, Response, SubMsg, SubMsgResponse, SubMsgResult, Timestamp, Uint128, WasmMsg, +}; +use cw20::{Cw20ExecuteMsg, Cw20ReceiveMsg, MinterResponse}; +use itertools::Itertools; +use proptest::prelude::*; +use prost::Message; + +use palomadex::asset::{native_asset, native_asset_info, Asset, AssetInfo}; +use palomadex::observation::query_observation; +use palomadex::observation::Observation; +use palomadex::observation::OracleObservation; +use palomadex::pair::{ + Cw20HookMsg, ExecuteMsg, InstantiateMsg, PoolResponse, SimulationResponse, StablePoolParams, +}; +use palomadex::token::InstantiateMsg as TokenInstantiateMsg; +use palomadex_circular_buffer::BufferManager; + +use crate::contract::{ + assert_max_spread, execute, instantiate, query_pool, query_reverse_simulation, query_share, + query_simulation, reply, +}; +use crate::error::ContractError; +use crate::mock_querier::mock_dependencies; +use crate::state::{CONFIG, OBSERVATIONS}; +use crate::utils::{compute_swap, select_pools}; + +#[derive(Clone, PartialEq, Message)] +struct MsgInstantiateContractResponse { + #[prost(string, tag = "1")] + pub contract_address: String, + #[prost(bytes, tag = "2")] + pub data: Vec, +} + +fn store_liquidity_token(deps: DepsMut, msg_id: u64, contract_addr: String) { + let instantiate_reply = MsgInstantiateContractResponse { + contract_address: contract_addr, + data: vec![], + }; + + let mut encoded_instantiate_reply = Vec::::with_capacity(instantiate_reply.encoded_len()); + instantiate_reply + .encode(&mut encoded_instantiate_reply) + .unwrap(); + + let reply_msg = Reply { + id: msg_id, + result: SubMsgResult::Ok(SubMsgResponse { + events: vec![], + data: Some(encoded_instantiate_reply.into()), + }), + }; + + reply(deps, mock_env(), reply_msg).unwrap(); +} + +#[test] +fn proper_initialization() { + let mut deps = mock_dependencies(&[]); + + deps.querier.with_token_balances(&[( + &String::from("asset0000"), + &[(&String::from(MOCK_CONTRACT_ADDR), &Uint128::new(123u128))], + )]); + + let msg = InstantiateMsg { + factory_addr: String::from("factory"), + asset_infos: vec![ + AssetInfo::NativeToken { + denom: "uusd".to_string(), + }, + AssetInfo::Token { + contract_addr: Addr::unchecked("asset0000"), + }, + ], + token_code_id: 10u64, + init_params: Some( + to_json_binary(&StablePoolParams { + amp: 100, + owner: None, + }) + .unwrap(), + ), + }; + + let sender = "addr0000"; + // We can just call .unwrap() to assert this was a success + let env = mock_env(); + let info = mock_info(sender, &[]); + let res = instantiate(deps.as_mut(), env, info, msg).unwrap(); + assert_eq!( + res.messages, + vec![SubMsg { + msg: WasmMsg::Instantiate { + code_id: 10u64, + msg: to_json_binary(&TokenInstantiateMsg { + name: "UUSD-MAPP-LP".to_string(), + symbol: "uLP".to_string(), + decimals: 6, + initial_balances: vec![], + mint: Some(MinterResponse { + minter: String::from(MOCK_CONTRACT_ADDR), + cap: None, + }), + marketing: None + }) + .unwrap(), + funds: vec![], + admin: None, + label: String::from("Palomadex LP token"), + } + .into(), + id: 1, + gas_limit: None, + reply_on: ReplyOn::Success + },] + ); + + // Store liquidity token + store_liquidity_token(deps.as_mut(), 1, "liquidity0000".to_string()); + + // It worked, let's query the state + let pair_info = CONFIG.load(deps.as_ref().storage).unwrap().pair_info; + assert_eq!(Addr::unchecked("liquidity0000"), pair_info.liquidity_token); + assert_eq!( + pair_info.asset_infos, + vec![ + AssetInfo::NativeToken { + denom: "uusd".to_string(), + }, + AssetInfo::Token { + contract_addr: Addr::unchecked("asset0000") + } + ] + ); +} + +#[test] +fn provide_liquidity() { + let mut deps = mock_dependencies(&[Coin { + denom: "uusd".to_string(), + amount: Uint128::new(200_000000000000000000u128), + }]); + + deps.querier.with_token_balances(&[ + ( + &String::from("asset0000"), + &[(&String::from(MOCK_CONTRACT_ADDR), &Uint128::new(0))], + ), + ( + &String::from("liquidity0000"), + &[(&String::from(MOCK_CONTRACT_ADDR), &Uint128::new(0))], + ), + ]); + + let msg = InstantiateMsg { + asset_infos: vec![ + AssetInfo::NativeToken { + denom: "uusd".to_string(), + }, + AssetInfo::Token { + contract_addr: Addr::unchecked("asset0000"), + }, + ], + token_code_id: 10u64, + factory_addr: String::from("factory"), + init_params: Some( + to_json_binary(&StablePoolParams { + amp: 100, + owner: None, + }) + .unwrap(), + ), + }; + + let env = mock_env(); + let info = mock_info("addr0000", &[]); + // We can just call .unwrap() to assert this was a success + let _res = instantiate(deps.as_mut(), env, info, msg).unwrap(); + + // Store the liquidity token + store_liquidity_token(deps.as_mut(), 1, "liquidity0000".to_string()); + + // Successfully provide liquidity for the existing pool + let msg = ExecuteMsg::ProvideLiquidity { + assets: vec![ + Asset { + info: AssetInfo::Token { + contract_addr: Addr::unchecked("asset0000"), + }, + amount: Uint128::from(100_000000000000000000u128), + }, + Asset { + info: AssetInfo::NativeToken { + denom: "uusd".to_string(), + }, + amount: Uint128::from(100_000000000000000000u128), + }, + ], + slippage_tolerance: None, + receiver: None, + }; + + let env = mock_env(); + let info = mock_info( + "addr0000", + &[Coin { + denom: "uusd".to_string(), + amount: Uint128::from(100_000000000000000000u128), + }], + ); + let res = execute(deps.as_mut(), env.clone().clone(), info, msg).unwrap(); + let transfer_from_msg = res.messages.get(0).expect("no message"); + let mint_min_liquidity_msg = res.messages.get(1).expect("no message"); + let mint_receiver_msg = res.messages.get(2).expect("no message"); + + assert_eq!( + transfer_from_msg, + &SubMsg { + msg: WasmMsg::Execute { + contract_addr: String::from("asset0000"), + msg: to_json_binary(&Cw20ExecuteMsg::TransferFrom { + owner: String::from("addr0000"), + recipient: String::from(MOCK_CONTRACT_ADDR), + amount: Uint128::from(100_000000000000000000u128), + }) + .unwrap(), + funds: vec![], + } + .into(), + id: 0, + gas_limit: None, + reply_on: ReplyOn::Never + } + ); + + assert_eq!( + mint_min_liquidity_msg, + &SubMsg { + msg: WasmMsg::Execute { + contract_addr: String::from("liquidity0000"), + msg: to_json_binary(&Cw20ExecuteMsg::Mint { + recipient: String::from(MOCK_CONTRACT_ADDR), + amount: Uint128::from(1000_u128), + }) + .unwrap(), + funds: vec![], + } + .into(), + id: 0, + gas_limit: None, + reply_on: ReplyOn::Never, + } + ); + + assert_eq!( + mint_receiver_msg, + &SubMsg { + msg: WasmMsg::Execute { + contract_addr: String::from("liquidity0000"), + msg: to_json_binary(&Cw20ExecuteMsg::Mint { + recipient: String::from("addr0000"), + amount: Uint128::from(299_814_698_523_989_456_628u128), + }) + .unwrap(), + funds: vec![], + } + .into(), + id: 0, + gas_limit: None, + reply_on: ReplyOn::Never, + } + ); + + // Provide more liquidity using a 1:2 ratio + deps.querier.with_balance(&[( + &String::from(MOCK_CONTRACT_ADDR), + &[Coin { + denom: "uusd".to_string(), + amount: Uint128::new(200_000000000000000000 + 200_000000000000000000 /* user deposit must be pre-applied */), + }], + )]); + + deps.querier.with_token_balances(&[ + ( + &String::from("liquidity0000"), + &[( + &String::from(MOCK_CONTRACT_ADDR), + &Uint128::new(100_000000000000000000), + )], + ), + ( + &String::from("asset0000"), + &[( + &String::from(MOCK_CONTRACT_ADDR), + &Uint128::new(200_000000000000000000), + )], + ), + ]); + + let msg = ExecuteMsg::ProvideLiquidity { + assets: vec![ + Asset { + info: AssetInfo::Token { + contract_addr: Addr::unchecked("asset0000"), + }, + amount: Uint128::from(100_000000000000000000u128), + }, + Asset { + info: AssetInfo::NativeToken { + denom: "uusd".to_string(), + }, + amount: Uint128::from(200_000000000000000000u128), + }, + ], + slippage_tolerance: None, + receiver: None, + }; + + let env = mock_env_with_block_time(env.block.time.seconds() + 1000); + let info = mock_info( + "addr0000", + &[Coin { + denom: "uusd".to_string(), + amount: Uint128::from(200_000000000000000000u128), + }], + ); + + let res: Response = execute(deps.as_mut(), env.clone(), info, msg).unwrap(); + let transfer_from_msg = res.messages.get(0).expect("no message"); + let mint_msg = res.messages.get(1).expect("no message"); + assert_eq!( + transfer_from_msg, + &SubMsg { + msg: WasmMsg::Execute { + contract_addr: String::from("asset0000"), + msg: to_json_binary(&Cw20ExecuteMsg::TransferFrom { + owner: String::from("addr0000"), + recipient: String::from(MOCK_CONTRACT_ADDR), + amount: Uint128::from(100_000000000000000000u128), + }) + .unwrap(), + funds: vec![], + } + .into(), + id: 0, + gas_limit: None, + reply_on: ReplyOn::Never, + } + ); + assert_eq!( + mint_msg, + &SubMsg { + msg: WasmMsg::Execute { + contract_addr: String::from("liquidity0000"), + msg: to_json_binary(&Cw20ExecuteMsg::Mint { + recipient: String::from("addr0000"), + amount: Uint128::new(74_981_956_874_579_206461), + }) + .unwrap(), + funds: vec![], + } + .into(), + id: 0, + gas_limit: None, + reply_on: ReplyOn::Never, + } + ); + + // Check wrong argument + let msg = ExecuteMsg::ProvideLiquidity { + assets: vec![ + Asset { + info: AssetInfo::Token { + contract_addr: Addr::unchecked("asset0000"), + }, + amount: Uint128::from(100_000000000000000000u128), + }, + Asset { + info: AssetInfo::NativeToken { + denom: "uusd".to_string(), + }, + amount: Uint128::from(50_000000000000000000u128), + }, + ], + slippage_tolerance: None, + receiver: None, + }; + + let env = mock_env(); + let info = mock_info( + "addr0000", + &[Coin { + denom: "uusd".to_string(), + amount: Uint128::from(100_000000000000000000u128), + }], + ); + let res = execute(deps.as_mut(), env.clone(), info, msg).unwrap_err(); + assert_eq!(res.to_string(), "Generic error: Native token balance mismatch between the argument (50000000000000000000uusd) and the transferred (100000000000000000000uusd)"); + + // Initialize token balances with a ratio of 1:1 + deps.querier.with_balance(&[( + &String::from(MOCK_CONTRACT_ADDR), + &[Coin { + denom: "uusd".to_string(), + amount: Uint128::new(100_000000000000000000 + 100_000000000000000000 /* user deposit must be pre-applied */), + }], + )]); + + deps.querier.with_token_balances(&[ + ( + &String::from("liquidity0000"), + &[( + &String::from(MOCK_CONTRACT_ADDR), + &Uint128::new(100_000000000000000000), + )], + ), + ( + &String::from("asset0000"), + &[( + &String::from(MOCK_CONTRACT_ADDR), + &Uint128::new(100_000000000000000000), + )], + ), + ]); + + // Initialize token balances with a ratio of 1:1 + deps.querier.with_balance(&[( + &String::from(MOCK_CONTRACT_ADDR), + &[Coin { + denom: "uusd".to_string(), + amount: Uint128::new(100_000000000000000000 + 98_000000000000000000 /* user deposit must be pre-applied */), + }], + )]); + + // Initialize token balances with a ratio of 1:1 + deps.querier.with_balance(&[( + &String::from(MOCK_CONTRACT_ADDR), + &[Coin { + denom: "uusd".to_string(), + amount: Uint128::new(100_000000000000000000 + 100_000000000000000000 /* user deposit must be pre-applied */), + }], + )]); + + // Successfully provide liquidity + let msg = ExecuteMsg::ProvideLiquidity { + assets: vec![ + Asset { + info: AssetInfo::Token { + contract_addr: Addr::unchecked("asset0000"), + }, + amount: Uint128::from(99_000000000000000000u128), + }, + Asset { + info: AssetInfo::NativeToken { + denom: "uusd".to_string(), + }, + amount: Uint128::from(100_000000000000000000u128), + }, + ], + slippage_tolerance: Some(Decimal::percent(1)), + receiver: None, + }; + + let env = mock_env_with_block_time(env.block.time.seconds() + 1000); + let info = mock_info( + "addr0001", + &[Coin { + denom: "uusd".to_string(), + amount: Uint128::from(100_000000000000000000u128), + }], + ); + let _res = execute(deps.as_mut(), env.clone(), info, msg).unwrap(); + + // Initialize token balances with a ratio of 1:1 + deps.querier.with_balance(&[( + &String::from(MOCK_CONTRACT_ADDR), + &[Coin { + denom: "uusd".to_string(), + amount: Uint128::new(100_000000000000000000 + 99_000000000000000000 /* user deposit must be pre-applied */), + }], + )]); + + // Successfully provide liquidity + let msg = ExecuteMsg::ProvideLiquidity { + assets: vec![ + Asset { + info: AssetInfo::Token { + contract_addr: Addr::unchecked("asset0000"), + }, + amount: Uint128::from(100_000000000000000000u128), + }, + Asset { + info: AssetInfo::NativeToken { + denom: "uusd".to_string(), + }, + amount: Uint128::from(99_000000000000000000u128), + }, + ], + slippage_tolerance: Some(Decimal::percent(1)), + receiver: None, + }; + + let env = mock_env_with_block_time(env.block.time.seconds() + 1000); + let info = mock_info( + "addr0001", + &[Coin { + denom: "uusd".to_string(), + amount: Uint128::from(99_000000000000000000u128), + }], + ); + let _res = execute(deps.as_mut(), env, info, msg).unwrap(); +} + +#[test] +fn withdraw_liquidity() { + let mut deps = mock_dependencies(&[Coin { + denom: "uusd".to_string(), + amount: Uint128::new(100u128), + }]); + + deps.querier.with_token_balances(&[ + ( + &String::from("liquidity0000"), + &[(&String::from("addr0000"), &Uint128::new(100u128))], + ), + ( + &String::from("asset0000"), + &[(&String::from(MOCK_CONTRACT_ADDR), &Uint128::new(100u128))], + ), + ]); + + let msg = InstantiateMsg { + asset_infos: vec![ + AssetInfo::NativeToken { + denom: "uusd".to_string(), + }, + AssetInfo::Token { + contract_addr: Addr::unchecked("asset0000"), + }, + ], + token_code_id: 10u64, + factory_addr: String::from("factory"), + init_params: Some( + to_json_binary(&StablePoolParams { + amp: 100, + owner: None, + }) + .unwrap(), + ), + }; + + let env = mock_env(); + let info = mock_info("addr0000", &[]); + // We can just call .unwrap() to assert this was a success + let _res = instantiate(deps.as_mut(), env, info, msg).unwrap(); + + // Store the liquidity token + store_liquidity_token(deps.as_mut(), 1, "liquidity0000".to_string()); + + // Withdraw liquidity + let msg = ExecuteMsg::Receive(Cw20ReceiveMsg { + sender: String::from("addr0000"), + msg: to_json_binary(&Cw20HookMsg::WithdrawLiquidity { assets: vec![] }).unwrap(), + amount: Uint128::new(100u128), + }); + + let env = mock_env(); + let info = mock_info("liquidity0000", &[]); + let res = execute(deps.as_mut(), env, info, msg).unwrap(); + let log_withdrawn_share = res.attributes.get(2).expect("no log"); + let log_refund_assets = res.attributes.get(3).expect("no log"); + let msg_refund_0 = res.messages.get(0).expect("no message"); + let msg_refund_1 = res.messages.get(1).expect("no message"); + let msg_burn_liquidity = res.messages.get(2).expect("no message"); + assert_eq!( + msg_refund_0, + &SubMsg { + msg: CosmosMsg::Bank(BankMsg::Send { + to_address: String::from("addr0000"), + amount: vec![Coin { + denom: "uusd".to_string(), + amount: Uint128::from(100u128), + }], + }), + id: 0, + gas_limit: None, + reply_on: ReplyOn::Never, + } + ); + assert_eq!( + msg_refund_1, + &SubMsg { + msg: WasmMsg::Execute { + contract_addr: String::from("asset0000"), + msg: to_json_binary(&Cw20ExecuteMsg::Transfer { + recipient: String::from("addr0000"), + amount: Uint128::from(100u128), + }) + .unwrap(), + funds: vec![], + } + .into(), + id: 0, + gas_limit: None, + reply_on: ReplyOn::Never, + } + ); + assert_eq!( + msg_burn_liquidity, + &SubMsg { + msg: WasmMsg::Execute { + contract_addr: String::from("liquidity0000"), + msg: to_json_binary(&Cw20ExecuteMsg::Burn { + amount: Uint128::from(100u128), + }) + .unwrap(), + funds: vec![], + } + .into(), + id: 0, + gas_limit: None, + reply_on: ReplyOn::Never, + } + ); + + assert_eq!( + log_withdrawn_share, + &attr("withdrawn_share", 100u128.to_string()) + ); + assert_eq!( + log_refund_assets, + &attr("refund_assets", "100uusd, 100asset0000") + ); +} + +#[test] +fn try_native_to_token() { + let total_share = Uint128::new(30000000000u128); + let asset_pool_amount = Uint128::new(20000000000u128); + let collateral_pool_amount = Uint128::new(30000000000u128); + let offer_amount = Uint128::new(1500000000u128); + + let mut deps = mock_dependencies(&[Coin { + denom: "uusd".to_string(), + amount: collateral_pool_amount + offer_amount, /* user deposit must be pre-applied */ + }]); + + deps.querier.with_token_balances(&[ + ( + &String::from("liquidity0000"), + &[(&String::from(MOCK_CONTRACT_ADDR), &total_share)], + ), + ( + &String::from("asset0000"), + &[(&String::from(MOCK_CONTRACT_ADDR), &asset_pool_amount)], + ), + ]); + + let msg = InstantiateMsg { + asset_infos: vec![ + AssetInfo::NativeToken { + denom: "uusd".to_string(), + }, + AssetInfo::Token { + contract_addr: Addr::unchecked("asset0000"), + }, + ], + token_code_id: 10u64, + factory_addr: String::from("factory"), + init_params: Some( + to_json_binary(&StablePoolParams { + amp: 100, + owner: None, + }) + .unwrap(), + ), + }; + + let env = mock_env_with_block_time(100); + let info = mock_info("addr0000", &[]); + // We can just call .unwrap() to assert this was a success + let _res = instantiate(deps.as_mut(), env, info, msg).unwrap(); + + // Store the liquidity token + store_liquidity_token(deps.as_mut(), 1, "liquidity0000".to_string()); + + // Normal swap + let msg = ExecuteMsg::Swap { + offer_asset: Asset { + info: AssetInfo::NativeToken { + denom: "uusd".to_string(), + }, + amount: offer_amount, + }, + ask_asset_info: None, + belief_price: None, + max_spread: Some(Decimal::percent(50)), + to: None, + }; + let env = mock_env_with_block_time(1000); + let info = mock_info( + "addr0000", + &[Coin { + denom: "uusd".to_string(), + amount: offer_amount, + }], + ); + + let res = execute(deps.as_mut(), env.clone(), info, msg).unwrap(); + let msg_transfer = res.messages.get(0).expect("no message"); + + let mut model: StableSwapModel = StableSwapModel::new( + 100, + vec![collateral_pool_amount.into(), asset_pool_amount.into()], + 2, + ); + + let sim_result = model.sim_exchange(0, 1, offer_amount.into()); + + let expected_ret_amount = Uint128::new(sim_result); + let expected_spread_amount = offer_amount.saturating_sub(expected_ret_amount); + let expected_commission_amount = expected_ret_amount.multiply_ratio(3u128, 1000u128); // 0.3% + let expected_maker_fee_amount = expected_commission_amount.multiply_ratio(166u128, 1000u128); + + let expected_return_amount = expected_ret_amount + .checked_sub(expected_commission_amount) + .unwrap(); + + // Check simulation result + deps.querier.with_balance(&[( + &String::from(MOCK_CONTRACT_ADDR), + &[Coin { + denom: "uusd".to_string(), + amount: collateral_pool_amount, /* user deposit must be pre-applied */ + }], + )]); + + let simulation_res: SimulationResponse = query_simulation( + deps.as_ref(), + env.clone(), + Asset { + info: AssetInfo::NativeToken { + denom: "uusd".to_string(), + }, + amount: offer_amount, + }, + None, + ) + .unwrap(); + assert!(expected_return_amount.abs_diff(simulation_res.return_amount) <= Uint128::one()); + assert_eq!(expected_commission_amount, simulation_res.commission_amount); + assert!(expected_spread_amount.abs_diff(simulation_res.spread_amount) <= Uint128::one()); + + assert_eq!( + res.attributes, + vec![ + attr("action", "swap"), + attr("sender", "addr0000"), + attr("receiver", "addr0000"), + attr("offer_asset", "uusd"), + attr("ask_asset", "asset0000"), + attr("offer_amount", offer_amount.to_string()), + attr("return_amount", 1487928894.to_string()), + attr("spread_amount", 7593888.to_string()), + attr("commission_amount", expected_commission_amount.to_string()), + attr("maker_fee_amount", expected_maker_fee_amount.to_string()), + attr("fee_share_amount", "0"), + ] + ); + + assert_eq!( + &SubMsg { + msg: WasmMsg::Execute { + contract_addr: String::from("asset0000"), + msg: to_json_binary(&Cw20ExecuteMsg::Transfer { + recipient: String::from("addr0000"), + amount: Uint128::from(1487928894u128), + }) + .unwrap(), + funds: vec![], + } + .into(), + id: 0, + gas_limit: None, + reply_on: ReplyOn::Never, + }, + msg_transfer, + ); +} + +#[test] +fn try_token_to_native() { + let total_share = Uint128::new(30000000000u128); + let asset_pool_amount = Uint128::new(20000000000u128); + let collateral_pool_amount = Uint128::new(30000000000u128); + let offer_amount = Uint128::new(1500000000u128); + + let mut deps = mock_dependencies(&[Coin { + denom: "uusd".to_string(), + amount: collateral_pool_amount, + }]); + deps.querier.with_token_balances(&[ + ( + &String::from("liquidity0000"), + &[(&String::from(MOCK_CONTRACT_ADDR), &total_share)], + ), + ( + &String::from("asset0000"), + &[( + &String::from(MOCK_CONTRACT_ADDR), + &(asset_pool_amount + offer_amount), + )], + ), + ]); + + let msg = InstantiateMsg { + asset_infos: vec![ + AssetInfo::NativeToken { + denom: "uusd".to_string(), + }, + AssetInfo::Token { + contract_addr: Addr::unchecked("asset0000"), + }, + ], + token_code_id: 10u64, + factory_addr: String::from("factory"), + init_params: Some( + to_json_binary(&StablePoolParams { + amp: 100, + owner: None, + }) + .unwrap(), + ), + }; + + let env = mock_env_with_block_time(100); + let info = mock_info("addr0000", &[]); + // We can just call .unwrap() to assert this was a success + let _res = instantiate(deps.as_mut(), env, info, msg).unwrap(); + + // Store the liquidity token + store_liquidity_token(deps.as_mut(), 1, "liquidity0000".to_string()); + + // Unauthorized access; can not execute swap directy for token swap + let msg = ExecuteMsg::Swap { + offer_asset: Asset { + info: AssetInfo::Token { + contract_addr: Addr::unchecked("asset0000"), + }, + amount: offer_amount, + }, + ask_asset_info: None, + belief_price: None, + max_spread: None, + to: None, + }; + let env = mock_env_with_block_time(1000); + let info = mock_info("addr0000", &[]); + let res = execute(deps.as_mut(), env, info, msg).unwrap_err(); + assert_eq!(res, ContractError::Cw20DirectSwap {}); + + // Normal sell + let msg = ExecuteMsg::Receive(Cw20ReceiveMsg { + sender: String::from("addr0000"), + amount: offer_amount, + msg: to_json_binary(&Cw20HookMsg::Swap { + ask_asset_info: None, + belief_price: None, + max_spread: None, + to: None, + }) + .unwrap(), + }); + let env = mock_env_with_block_time(1000); + let info = mock_info("asset0000", &[]); + + let res = execute(deps.as_mut(), env.clone(), info, msg).unwrap(); + let msg_transfer = res.messages.get(0).expect("no message"); + + let mut model: StableSwapModel = StableSwapModel::new( + 100, + vec![collateral_pool_amount.into(), asset_pool_amount.into()], + 2, + ); + + let sim_result = model.sim_exchange(1, 0, offer_amount.into()); + + let expected_ret_amount = Uint128::new(sim_result); + let expected_spread_amount = offer_amount.saturating_sub(expected_ret_amount); + let expected_commission_amount = expected_ret_amount.multiply_ratio(3u128, 1000u128); // 0.3% + let expected_maker_fee_amount = expected_commission_amount.multiply_ratio(166u128, 1000u128); + + let expected_return_amount = expected_ret_amount + .checked_sub(expected_commission_amount) + .unwrap(); + + // Check simulation result + // Return asset token balance as normal + deps.querier.with_token_balances(&[ + ( + &String::from("liquidity0000"), + &[(&String::from(MOCK_CONTRACT_ADDR), &total_share)], + ), + ( + &String::from("asset0000"), + &[(&String::from(MOCK_CONTRACT_ADDR), &(asset_pool_amount))], + ), + ]); + + let simulation_res: SimulationResponse = query_simulation( + deps.as_ref(), + env.clone(), + Asset { + amount: offer_amount, + info: AssetInfo::Token { + contract_addr: Addr::unchecked("asset0000"), + }, + }, + None, + ) + .unwrap(); + assert!(expected_return_amount.abs_diff(simulation_res.return_amount) <= Uint128::one()); + assert_eq!(expected_commission_amount, simulation_res.commission_amount); + assert!(expected_spread_amount.abs_diff(simulation_res.spread_amount) <= Uint128::one()); + + assert_eq!( + res.attributes, + vec![ + attr("action", "swap"), + attr("sender", "addr0000"), + attr("receiver", "addr0000"), + attr("offer_asset", "asset0000"), + attr("ask_asset", "uusd"), + attr("offer_amount", offer_amount.to_string()), + attr("return_amount", 1500851252.to_string()), + attr("spread_amount", expected_spread_amount.to_string()), + attr("commission_amount", expected_commission_amount.to_string()), + attr("maker_fee_amount", expected_maker_fee_amount.to_string()), + attr("fee_share_amount", "0"), + ] + ); + + assert_eq!( + &SubMsg { + msg: CosmosMsg::Bank(BankMsg::Send { + to_address: String::from("addr0000"), + amount: vec![Coin { + denom: "uusd".to_string(), + amount: 1500851252u128.into(), + }], + }) + .into(), + id: 0, + gas_limit: None, + reply_on: ReplyOn::Never, + }, + msg_transfer, + ); + + // Failed due to non asset token contract being used in a swap + let msg = ExecuteMsg::Receive(Cw20ReceiveMsg { + sender: String::from("addr0000"), + amount: offer_amount, + msg: to_json_binary(&Cw20HookMsg::Swap { + ask_asset_info: None, + belief_price: None, + max_spread: None, + to: None, + }) + .unwrap(), + }); + let env = mock_env_with_block_time(1000); + let info = mock_info("liquidtity0000", &[]); + let res = execute(deps.as_mut(), env, info, msg).unwrap_err(); + assert_eq!(res, ContractError::Unauthorized {}); +} + +#[test] +fn test_max_spread() { + assert_max_spread( + Some(Decimal::from_ratio(1200u128, 1u128)), + Some(Decimal::percent(1)), + Uint128::from(1200000000u128), + Uint128::from(989999u128), + Uint128::zero(), + ) + .unwrap_err(); + + assert_max_spread( + Some(Decimal::from_ratio(1200u128, 1u128)), + Some(Decimal::percent(1)), + Uint128::from(1200000000u128), + Uint128::from(990000u128), + Uint128::zero(), + ) + .unwrap(); + + assert_max_spread( + None, + Some(Decimal::percent(1)), + Uint128::zero(), + Uint128::from(989999u128), + Uint128::from(10001u128), + ) + .unwrap_err(); + + assert_max_spread( + None, + Some(Decimal::percent(1)), + Uint128::zero(), + Uint128::from(990000u128), + Uint128::from(10000u128), + ) + .unwrap(); +} + +#[test] +fn test_query_pool() { + let total_share_amount = Uint128::from(111u128); + let asset_0_amount = Uint128::from(222u128); + let asset_1_amount = Uint128::from(333u128); + let mut deps = mock_dependencies(&[Coin { + denom: "uusd".to_string(), + amount: asset_0_amount, + }]); + + deps.querier.with_token_balances(&[ + ( + &String::from("asset0000"), + &[(&String::from(MOCK_CONTRACT_ADDR), &asset_1_amount)], + ), + ( + &String::from("liquidity0000"), + &[(&String::from(MOCK_CONTRACT_ADDR), &total_share_amount)], + ), + ]); + + let msg = InstantiateMsg { + asset_infos: vec![ + AssetInfo::NativeToken { + denom: "uusd".to_string(), + }, + AssetInfo::Token { + contract_addr: Addr::unchecked("asset0000"), + }, + ], + token_code_id: 10u64, + factory_addr: String::from("factory"), + init_params: Some( + to_json_binary(&StablePoolParams { + amp: 100, + owner: None, + }) + .unwrap(), + ), + }; + + let env = mock_env(); + let info = mock_info("addr0000", &[]); + // We can just call .unwrap() to assert this was a success + let _res = instantiate(deps.as_mut(), env, info, msg).unwrap(); + + // Store the liquidity token + store_liquidity_token(deps.as_mut(), 1, "liquidity0000".to_string()); + + let res: PoolResponse = query_pool(deps.as_ref()).unwrap(); + + assert_eq!( + res.assets, + [ + Asset { + info: AssetInfo::NativeToken { + denom: "uusd".to_string(), + }, + amount: asset_0_amount + }, + Asset { + info: AssetInfo::Token { + contract_addr: Addr::unchecked("asset0000"), + }, + amount: asset_1_amount + } + ] + ); + assert_eq!(res.total_share, total_share_amount); +} + +#[test] +fn test_query_share() { + let total_share_amount = Uint128::from(500u128); + let asset_0_amount = Uint128::from(250u128); + let asset_1_amount = Uint128::from(1000u128); + let mut deps = mock_dependencies(&[Coin { + denom: "uusd".to_string(), + amount: asset_0_amount, + }]); + + deps.querier.with_token_balances(&[ + ( + &String::from("asset0000"), + &[(&String::from(MOCK_CONTRACT_ADDR), &asset_1_amount)], + ), + ( + &String::from("liquidity0000"), + &[(&String::from(MOCK_CONTRACT_ADDR), &total_share_amount)], + ), + ]); + + let msg = InstantiateMsg { + asset_infos: vec![ + AssetInfo::NativeToken { + denom: "uusd".to_string(), + }, + AssetInfo::Token { + contract_addr: Addr::unchecked("asset0000"), + }, + ], + token_code_id: 10u64, + factory_addr: String::from("factory"), + init_params: Some( + to_json_binary(&StablePoolParams { + amp: 100, + owner: None, + }) + .unwrap(), + ), + }; + + let env = mock_env(); + let info = mock_info("addr0000", &[]); + // We can just call .unwrap() to assert this was a success + let _res = instantiate(deps.as_mut(), env, info, msg).unwrap(); + + // Store the liquidity token + store_liquidity_token(deps.as_mut(), 1, "liquidity0000".to_string()); + + let res = query_share(deps.as_ref(), Uint128::new(250)).unwrap(); + + assert_eq!(res[0].amount, Uint128::new(125)); + assert_eq!(res[1].amount, Uint128::new(500)); +} + +pub fn f64_to_dec(val: f64) -> T +where + T: FromStr, + T::Err: Error, +{ + T::from_str(&val.to_string()).unwrap() +} + +#[test] +fn observations_full_buffer() { + let mut deps = mock_dependencies(&[]); + let mut env = mock_env(); + env.block.time = Timestamp::from_seconds(100_000); + BufferManager::init(&mut deps.storage, OBSERVATIONS, 20).unwrap(); + + let mut buffer = BufferManager::new(&deps.storage, OBSERVATIONS).unwrap(); + + let err = query_observation(deps.as_ref(), env.clone(), OBSERVATIONS, 11000).unwrap_err(); + assert_eq!(err.to_string(), "Generic error: Buffer is empty"); + + let array = (1..=30) + .into_iter() + .map(|i| Observation { + ts: env.block.time.seconds() + i * 1000, + price: Default::default(), + price_sma: Decimal::from_ratio(i, i * i), + }) + .collect_vec(); + buffer.push_many(&array); + buffer.commit(&mut deps.storage).unwrap(); + + env.block.time = env.block.time.plus_seconds(30_000); + + assert_eq!( + OracleObservation { + timestamp: 120_000, + price: f64_to_dec(20.0 / 400.0), + }, + query_observation(deps.as_ref(), env.clone(), OBSERVATIONS, 10000).unwrap() + ); + + assert_eq!( + OracleObservation { + timestamp: 124_411, + price: f64_to_dec(0.04098166666666694), + }, + query_observation(deps.as_ref(), env.clone(), OBSERVATIONS, 5589).unwrap() + ); + + let err = query_observation(deps.as_ref(), env, OBSERVATIONS, 35_000).unwrap_err(); + assert_eq!( + err.to_string(), + "Generic error: Requested observation is too old. Last known observation is at 111000" + ); +} + +#[test] +fn observations_incomplete_buffer() { + let mut deps = mock_dependencies(&[]); + let mut env = mock_env(); + env.block.time = Timestamp::from_seconds(100_000); + BufferManager::init(&mut deps.storage, OBSERVATIONS, 3000).unwrap(); + + let mut buffer = BufferManager::new(&deps.storage, OBSERVATIONS).unwrap(); + + let err = query_observation(deps.as_ref(), env.clone(), OBSERVATIONS, 11000).unwrap_err(); + assert_eq!(err.to_string(), "Generic error: Buffer is empty"); + + let array = (1..=30) + .into_iter() + .map(|i| Observation { + ts: env.block.time.seconds() + i * 1000, + price: Default::default(), + price_sma: Decimal::from_ratio(i, i * i), + }) + .collect_vec(); + buffer.push_many(&array); + buffer.commit(&mut deps.storage).unwrap(); + + env.block.time = env.block.time.plus_seconds(30_000); + + assert_eq!( + OracleObservation { + timestamp: 120_000, + price: f64_to_dec(20.0 / 400.0), + }, + query_observation(deps.as_ref(), env.clone(), OBSERVATIONS, 10000).unwrap() + ); + + assert_eq!( + OracleObservation { + timestamp: 124_411, + price: f64_to_dec(0.04098166666666694), + }, + query_observation(deps.as_ref(), env.clone(), OBSERVATIONS, 5589).unwrap() + ); +} + +#[test] +fn observations_checking_triple_capacity_step_by_step() { + let mut deps = mock_dependencies(&[]); + let mut env = mock_env(); + env.block.time = Timestamp::from_seconds(100_000); + const CAPACITY: u32 = 20; + BufferManager::init(&mut deps.storage, OBSERVATIONS, CAPACITY).unwrap(); + + let mut buffer = BufferManager::new(&deps.storage, OBSERVATIONS).unwrap(); + + let ts = env.block.time.seconds(); + + let array = (1..=CAPACITY * 3) + .into_iter() + .map(|i| Observation { + ts: ts + i as u64 * 1000, + price: Default::default(), + price_sma: Decimal::from_ratio(i * i, i), + }) + .collect_vec(); + + for (k, obs) in array.iter().enumerate() { + env.block.time = env.block.time.plus_seconds(1000); + + buffer.push(&obs); + buffer.commit(&mut deps.storage).unwrap(); + let k1 = k as u32 + 1; + + let from = k1.saturating_sub(CAPACITY) + 1; + let to = k1; + + for i in from..=to { + let shift = (to - i) as u64; + if shift != 0 { + assert_eq!( + OracleObservation { + timestamp: ts + i as u64 * 1000 + 500, + price: f64_to_dec(i as f64 + 0.5), + }, + query_observation(deps.as_ref(), env.clone(), OBSERVATIONS, shift * 1000 - 500) + .unwrap() + ); + } + assert_eq!( + OracleObservation { + timestamp: ts + i as u64 * 1000, + price: f64_to_dec(i as f64), + }, + query_observation(deps.as_ref(), env.clone(), OBSERVATIONS, shift * 1000).unwrap() + ); + } + } +} + +fn mock_env_with_block_time(time: u64) -> Env { + let mut env = mock_env(); + env.block = BlockInfo { + height: 1, + time: Timestamp::from_seconds(time), + chain_id: "columbus".to_string(), + }; + env +} + +proptest! { + #[test] + fn constant_product_swap_no_fee( + balance_in in 1000..1_000_000_000_000_000_000u128, + balance_out in 1000..1_000_000_000_000_000_000u128, + amount_in in 1000..100_000_000_000u128, + amp in 1..150u64 + ) { + prop_assume!(amount_in < balance_in && balance_out > balance_in); + + let offer_asset = native_asset("uusd".to_string(), Uint128::from(amount_in)); + let ask_asset = native_asset_info("uluna".to_string()); + + let msg = InstantiateMsg { + factory_addr: String::from("factory"), + asset_infos: vec![offer_asset.info.clone(), ask_asset.clone()], + token_code_id: 10u64, + init_params: Some(to_json_binary(&StablePoolParams { amp, owner: None }).unwrap()), + }; + + let env = mock_env(); + let info = mock_info("owner", &[]); + let mut deps = mock_dependencies(&[coin(balance_in, "uusd"), coin(balance_out, "uluna")]); + + instantiate(deps.as_mut(), env.clone(), info, msg).unwrap(); + let config = CONFIG.load(deps.as_ref().storage).unwrap(); + let pools = config + .pair_info + .query_pools_decimal(&deps.as_ref().querier, &env.contract.address, &config.factory_addr) + .unwrap(); + let (offer_pool, ask_pool) = + select_pools(Some(&offer_asset.info), None, &pools).unwrap(); + + let result = compute_swap( + deps.as_ref().storage, + &env, + &config, + &offer_asset.to_decimal_asset(offer_asset.info.decimals(&deps.as_ref().querier, &config.factory_addr).unwrap()).unwrap(), + &offer_pool, + &ask_pool, + &pools, + ) + .unwrap(); + + let mut model: StableSwapModel = StableSwapModel::new(amp.into(), vec![balance_in, balance_out], 2); + let sim_result = model.sim_exchange(0, 1, amount_in); + + let diff = (sim_result as i128 - result.return_amount.u128() as i128).abs(); + + assert!( + diff <= 20, + "result={}, sim_result={}, amp={}, amount_in={}, balance_in={}, balance_out={}, diff={}", + result.return_amount, + sim_result, + amp, + amount_in, + balance_in, + balance_out, + diff + ); + + let reverse_result = query_reverse_simulation( + deps.as_ref(), + env.clone(), + native_asset("uluna".to_string(), result.return_amount), + None, + ) + .unwrap(); + + let amount_in_f = amount_in as f64; + let reverse_diff = + (reverse_result.offer_amount.u128() as f64 - amount_in_f) / amount_in_f * 100.; + + assert!( + reverse_diff <= 0.5, + "result={}, sim_result={}, amp={}, amount_out={}, balance_in={}, balance_out={}, diff(%)={}", + reverse_result.offer_amount.u128(), + amount_in, + amp, + result.return_amount.u128(), + balance_in, + balance_out, + reverse_diff + ); + } +} diff --git a/contracts/pair_stable/src/utils.rs b/contracts/pair_stable/src/utils.rs new file mode 100644 index 0000000..b30e7cf --- /dev/null +++ b/contracts/pair_stable/src/utils.rs @@ -0,0 +1,386 @@ +use std::cmp::Ordering; + +use cosmwasm_std::{ + wasm_execute, Addr, Api, CosmosMsg, Decimal, Decimal256, Env, StdResult, Storage, Uint128, + Uint64, +}; +use cw20::Cw20ExecuteMsg; +use itertools::Itertools; + +use palomadex::asset::{Asset, AssetInfo, Decimal256Ext, DecimalAsset}; +use palomadex::observation::{ + safe_sma_buffer_not_full, safe_sma_calculation, Observation, PrecommitObservation, +}; +use palomadex::pair::TWAP_PRECISION; +use palomadex_circular_buffer::error::BufferResult; +use palomadex_circular_buffer::BufferManager; + +use crate::error::ContractError; +use crate::math::calc_y; +use crate::state::{get_precision, Config, OBSERVATIONS}; + +/// Helper function to check if the given asset infos are valid. +pub(crate) fn check_asset_infos( + api: &dyn Api, + asset_infos: &[AssetInfo], +) -> Result<(), ContractError> { + if !asset_infos.iter().all_unique() { + return Err(ContractError::DoublingAssets {}); + } + + asset_infos + .iter() + .try_for_each(|asset_info| asset_info.check(api)) + .map_err(Into::into) +} + +/// Helper function to check that the assets in a given array are valid. +pub(crate) fn check_assets(api: &dyn Api, assets: &[Asset]) -> Result<(), ContractError> { + let asset_infos = assets.iter().map(|asset| asset.info.clone()).collect_vec(); + check_asset_infos(api, &asset_infos) +} + +/// Checks that cw20 token is part of the pool. +/// +/// * **cw20_sender** is cw20 token address which is being checked. +pub(crate) fn check_cw20_in_pool(config: &Config, cw20_sender: &Addr) -> Result<(), ContractError> { + for asset_info in &config.pair_info.asset_infos { + match asset_info { + AssetInfo::Token { contract_addr } if contract_addr == cw20_sender => return Ok(()), + _ => {} + } + } + + Err(ContractError::Unauthorized {}) +} + +/// Select offer and ask pools based on given offer and ask infos. +/// This function works with pools with up to 5 assets. Returns (offer_pool, ask_pool) in case of success. +/// If it is impossible to define offer and ask pools, returns [`ContractError`]. +/// +/// * **offer_asset_info** - asset info of the offer asset. +/// +/// * **ask_asset_info** - asset info of the ask asset. +/// +/// * **pools** - list of pools. +pub(crate) fn select_pools( + offer_asset_info: Option<&AssetInfo>, + ask_asset_info: Option<&AssetInfo>, + pools: &[DecimalAsset], +) -> Result<(DecimalAsset, DecimalAsset), ContractError> { + if pools.len() == 2 { + match (offer_asset_info, ask_asset_info) { + (Some(offer_asset_info), _) => { + let (offer_ind, offer_pool) = pools + .iter() + .find_position(|pool| pool.info.eq(offer_asset_info)) + .ok_or(ContractError::AssetMismatch {})?; + Ok((offer_pool.clone(), pools[(offer_ind + 1) % 2].clone())) + } + (_, Some(ask_asset_info)) => { + let (ask_ind, ask_pool) = pools + .iter() + .find_position(|pool| pool.info.eq(ask_asset_info)) + .ok_or(ContractError::AssetMismatch {})?; + Ok((pools[(ask_ind + 1) % 2].clone(), ask_pool.clone())) + } + _ => Err(ContractError::VariableAssetMissed {}), // Should always be unreachable + } + } else if let (Some(offer_asset_info), Some(ask_asset_info)) = + (offer_asset_info, ask_asset_info) + { + if ask_asset_info.eq(offer_asset_info) { + return Err(ContractError::SameAssets {}); + } + + let offer_pool = pools + .iter() + .find(|pool| pool.info.eq(offer_asset_info)) + .ok_or(ContractError::AssetMismatch {})?; + let ask_pool = pools + .iter() + .find(|pool| pool.info.eq(ask_asset_info)) + .ok_or(ContractError::AssetMismatch {})?; + + Ok((offer_pool.clone(), ask_pool.clone())) + } else { + Err(ContractError::VariableAssetMissed {}) // Should always be unreachable + } +} + +/// Compute the current pool amplification coefficient (AMP). +pub fn compute_current_amp(config: &Config, env: &Env) -> StdResult { + let block_time = env.block.time.seconds(); + if block_time < config.next_amp_time { + let elapsed_time: Uint128 = block_time.saturating_sub(config.init_amp_time).into(); + let time_range = config + .next_amp_time + .saturating_sub(config.init_amp_time) + .into(); + let init_amp = Uint128::from(config.init_amp); + let next_amp = Uint128::from(config.next_amp); + + if next_amp > init_amp { + let amp_range = next_amp - init_amp; + let res = init_amp + (amp_range * elapsed_time).checked_div(time_range)?; + Ok(res.try_into()?) + } else { + let amp_range = init_amp - next_amp; + let res = init_amp - (amp_range * elapsed_time).checked_div(time_range)?; + Ok(res.try_into()?) + } + } else { + Ok(Uint64::from(config.next_amp)) + } +} + +/// Returns a value using a newly specified precision. +/// +/// * **value** value that will have its precision adjusted. +/// +/// * **current_precision** `value`'s current precision +/// +/// * **new_precision** new precision to use when returning the `value`. +pub(crate) fn adjust_precision( + value: Uint128, + current_precision: u8, + new_precision: u8, +) -> StdResult { + Ok(match current_precision.cmp(&new_precision) { + Ordering::Equal => value, + Ordering::Less => value.checked_mul(Uint128::new( + 10_u128.pow((new_precision - current_precision) as u32), + ))?, + Ordering::Greater => value.checked_div(Uint128::new( + 10_u128.pow((current_precision - new_precision) as u32), + ))?, + }) +} + +/// Mint LP tokens for a beneficiary and auto stake the tokens in the Generator contract (if auto staking is specified). +/// +/// * **recipient** LP token recipient. +/// +/// * **amount** amount of LP tokens that will be minted for the recipient. +/// +/// * **auto_stake** whether the newly minted LP tokens will be automatically staked in the Generator on behalf of the recipient. +pub(crate) fn mint_liquidity_token_message( + config: &Config, + recipient: &Addr, + amount: Uint128, +) -> Result, ContractError> { + let lp_token = &config.pair_info.liquidity_token; + + Ok(vec![wasm_execute( + lp_token, + &Cw20ExecuteMsg::Mint { + recipient: recipient.to_string(), + amount, + }, + vec![], + )? + .into()]) +} + +/// Return the amount of tokens that a specific amount of LP tokens would withdraw. +/// +/// * **pools** array with assets available in the pool. +/// +/// * **amount** amount of LP tokens to calculate underlying amounts for. +/// +/// * **total_share** total amount of LP tokens currently issued by the pool. +pub(crate) fn get_share_in_assets( + pools: &[Asset], + amount: Uint128, + total_share: Uint128, +) -> Vec { + let mut share_ratio = Decimal::zero(); + if !total_share.is_zero() { + share_ratio = Decimal::from_ratio(amount, total_share); + } + + pools + .iter() + .map(|pool| Asset { + info: pool.info.clone(), + amount: pool.amount * share_ratio, + }) + .collect() +} + +/// Structure for internal use which represents swap result. +pub(crate) struct SwapResult { + pub return_amount: Uint128, + pub spread_amount: Uint128, +} + +/// Returns the result of a swap in form of a [`SwapResult`] object. +/// +/// * **offer_asset** asset that is being offered. +/// +/// * **offer_pool** pool of offered asset. +/// +/// * **ask_pool** asked asset. +/// +/// * **pools** array with assets available in the pool. +pub(crate) fn compute_swap( + storage: &dyn Storage, + env: &Env, + config: &Config, + offer_asset: &DecimalAsset, + offer_pool: &DecimalAsset, + ask_pool: &DecimalAsset, + pools: &[DecimalAsset], +) -> Result { + let token_precision = get_precision(storage, &ask_pool.info)?; + let xp = pools.iter().map(|p| p.amount).collect_vec(); + + let new_ask_pool = calc_y( + compute_current_amp(config, env)?, + offer_pool.amount + offer_asset.amount, + &xp, + token_precision, + )?; + + let return_amount = ask_pool.amount.to_uint128_with_precision(token_precision)? - new_ask_pool; + let offer_asset_amount = offer_asset + .amount + .to_uint128_with_precision(token_precision)?; + + // We consider swap rate 1:1 in stable swap thus any difference is considered as spread. + let spread_amount = offer_asset_amount.saturating_sub(return_amount); + + Ok(SwapResult { + return_amount, + spread_amount, + }) +} + +/// Accumulate token prices for the assets in the pool. +/// +/// * **pools** array with assets available in the pool. +pub fn accumulate_prices( + storage: &dyn Storage, + env: &Env, + config: &mut Config, + pools: &[DecimalAsset], +) -> Result { + let block_time = env.block.time.seconds(); + if block_time <= config.block_time_last { + return Ok(false); + } + + let time_elapsed = Uint128::from(block_time - config.block_time_last); + + if pools.iter().all(|pool| !pool.amount.is_zero()) { + let immut_config = config.clone(); + for (from, to, value) in config.cumulative_prices.iter_mut() { + let offer_asset = DecimalAsset { + info: from.clone(), + amount: Decimal256::one(), + }; + + let (offer_pool, ask_pool) = select_pools(Some(from), Some(to), pools)?; + let SwapResult { return_amount, .. } = compute_swap( + storage, + env, + &immut_config, + &offer_asset, + &offer_pool, + &ask_pool, + pools, + )?; + + *value = value.wrapping_add(time_elapsed.checked_mul(adjust_precision( + return_amount, + get_precision(storage, &ask_pool.info)?, + TWAP_PRECISION, + )?)?); + } + } + + config.block_time_last = block_time; + + Ok(true) +} + +/// Calculate and save price moving average +pub fn accumulate_swap_sizes(storage: &mut dyn Storage, env: &Env) -> BufferResult<()> { + if let Some(PrecommitObservation { + base_amount, + quote_amount, + precommit_ts, + }) = PrecommitObservation::may_load(storage)? + { + let mut buffer = BufferManager::new(storage, OBSERVATIONS)?; + let observed_price = Decimal::from_ratio(base_amount, quote_amount); + + let new_observation; + if let Some(last_obs) = buffer.read_last(storage)? { + // Skip saving observation if it has been already saved + if last_obs.ts < precommit_ts { + // Since this is circular buffer the next index contains the oldest value + let count = buffer.capacity(); + if let Some(oldest_obs) = buffer.read_single(storage, buffer.head() + 1)? { + let price_sma = safe_sma_calculation( + last_obs.price_sma, + oldest_obs.price, + count, + observed_price, + )?; + new_observation = Observation { + ts: precommit_ts, + price: observed_price, + price_sma, + }; + } else { + // Buffer is not full yet + let count = buffer.head(); + let price_sma = + safe_sma_buffer_not_full(last_obs.price_sma, count, observed_price)?; + new_observation = Observation { + ts: precommit_ts, + price: observed_price, + price_sma, + }; + } + + buffer.instant_push(storage, &new_observation)? + } + } else { + // Buffer is empty + if env.block.time.seconds() > precommit_ts { + new_observation = Observation { + ts: precommit_ts, + price: observed_price, + price_sma: observed_price, + }; + + buffer.instant_push(storage, &new_observation)? + } + } + } + + Ok(()) +} + +/// Internal function to determine which asset is base one, which is quote one +pub(crate) fn determine_base_quote_amount( + pools: &[DecimalAsset], + offer_asset: &Asset, + return_amount: Uint128, +) -> Result<(Uint128, Uint128), ContractError> { + let offer_index = pools + .iter() + .find_position(|asset| asset.info == offer_asset.info) + .ok_or_else(|| ContractError::InvalidAsset(offer_asset.info.to_string()))? + .0; + + let (base_amount, quote_amount) = if offer_index == 0 { + (offer_asset.amount, return_amount) + } else { + (return_amount, offer_asset.amount) + }; + + Ok((base_amount, quote_amount)) +} diff --git a/contracts/pair_stable/tests/helper.rs b/contracts/pair_stable/tests/helper.rs new file mode 100644 index 0000000..5002383 --- /dev/null +++ b/contracts/pair_stable/tests/helper.rs @@ -0,0 +1,530 @@ +#![cfg(not(tarpaulin_include))] + +use std::collections::HashMap; +use std::error::Error; +use std::str::FromStr; + +use anyhow::Result as AnyResult; +use cosmwasm_std::{coin, to_json_binary, Addr, Coin, Decimal, Empty, StdResult, Uint128}; +use cw20::{BalanceResponse, Cw20Coin, Cw20ExecuteMsg, Cw20QueryMsg}; +use derivative::Derivative; +use itertools::Itertools; +use palomadex_mocks::cw_multi_test::{App, AppResponse, Contract, ContractWrapper, Executor}; + +use palomadex::asset::{native_asset_info, token_asset_info, Asset, AssetInfo, PairInfo}; +use palomadex::factory::{PairConfig, PairType}; +use palomadex::observation::OracleObservation; +use palomadex::pair::{ + CumulativePricesResponse, Cw20HookMsg, ExecuteMsg, QueryMsg, ReverseSimulationResponse, + SimulationResponse, StablePoolParams, +}; +pub const NATIVE_TOKEN_PRECISION: u8 = 6; +use palomadex_pair_stable::contract::{execute, instantiate, query, reply}; + +const INIT_BALANCE: u128 = 1_000_000_000000; + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub enum TestCoin { + Cw20(String), + Cw20Precise(String, u8), + Native(String), +} + +impl TestCoin { + pub fn denom(&self) -> Option { + match self { + TestCoin::Native(denom) => Some(denom.clone()), + _ => None, + } + } + + pub fn cw20_init_data(&self) -> Option<(String, u8)> { + match self { + TestCoin::Cw20(name) => Some((name.clone(), 6u8)), + TestCoin::Cw20Precise(name, precision) => Some((name.clone(), *precision)), + _ => None, + } + } + + pub fn native(denom: &str) -> Self { + Self::Native(denom.to_string()) + } + + pub fn cw20(name: &str) -> Self { + Self::Cw20(name.to_string()) + } + + pub fn cw20precise(name: &str, precision: u8) -> Self { + Self::Cw20Precise(name.to_string(), precision) + } +} + +pub fn init_native_coins(test_coins: &[TestCoin]) -> Vec { + test_coins + .iter() + .filter_map(|test_coin| match test_coin { + TestCoin::Native(name) => { + let init_balance = INIT_BALANCE * 10u128.pow(NATIVE_TOKEN_PRECISION as u32); + Some(coin(init_balance, name)) + } + _ => None, + }) + .collect() +} + +fn token_contract() -> Box> { + Box::new(ContractWrapper::new_with_empty( + cw20_base::contract::execute, + cw20_base::contract::instantiate, + cw20_base::contract::query, + )) +} + +fn pair_contract() -> Box> { + Box::new(ContractWrapper::new_with_empty(execute, instantiate, query).with_reply_empty(reply)) +} + +fn factory_contract() -> Box> { + Box::new( + ContractWrapper::new_with_empty( + palomadex_factory::contract::execute, + palomadex_factory::contract::instantiate, + palomadex_factory::contract::query, + ) + .with_reply_empty(palomadex_factory::contract::reply), + ) +} + +fn store_coin_registry_code() -> Box> { + Box::new(ContractWrapper::new_with_empty( + palomadex_native_coin_registry::contract::execute, + palomadex_native_coin_registry::contract::instantiate, + palomadex_native_coin_registry::contract::query, + )) +} + +#[derive(Derivative)] +#[derivative(Debug)] +pub struct Helper { + #[derivative(Debug = "ignore")] + pub app: App, + pub owner: Addr, + pub assets: HashMap, + pub factory: Addr, + pub pair_addr: Addr, + pub lp_token: Addr, + pub amp: u64, +} + +impl Helper { + pub fn new( + owner: &Addr, + test_coins: Vec, + amp: u64, + swap_fee: Option, + ) -> AnyResult { + let mut app = App::new(|router, _, storage| { + router + .bank + .init_balance(storage, &owner, init_native_coins(&test_coins)) + .unwrap() + }); + + let mut asset_infos_vec: Vec<_> = test_coins + .clone() + .into_iter() + .filter_map(|coin| Some((coin.clone(), native_asset_info(coin.denom()?)))) + .collect(); + + let token_code_id = app.store_code(token_contract()); + + test_coins.clone().into_iter().for_each(|coin| { + if let Some((name, decimals)) = coin.cw20_init_data() { + let token_addr = Self::init_token(&mut app, token_code_id, name, decimals, &owner); + asset_infos_vec.push((coin, token_asset_info(token_addr))) + } + }); + + let coin_registry_id = app.store_code(store_coin_registry_code()); + let coin_registry_address = app + .instantiate_contract( + coin_registry_id, + Addr::unchecked(owner.to_string()), + &palomadex::native_coin_registry::InstantiateMsg { + owner: owner.to_string(), + }, + &[], + "Coin registry", + None, + ) + .unwrap(); + + app.execute_contract( + Addr::unchecked(owner.to_string()), + coin_registry_address.clone(), + &palomadex::native_coin_registry::ExecuteMsg::Add { + native_coins: vec![ + ("one".to_string(), 6), + ("three".to_string(), 6), + ("five".to_string(), 6), + ("uusd".to_string(), 6), + ("ibc/usd".to_string(), 6), + ("uluna".to_string(), 6), + ], + }, + &[], + ) + .unwrap(); + + let pair_code_id = app.store_code(pair_contract()); + let factory_code_id = app.store_code(factory_contract()); + + let init_msg = palomadex::factory::InstantiateMsg { + fee_address: None, + pair_configs: vec![PairConfig { + code_id: pair_code_id, + maker_fee_bps: 5000, + total_fee_bps: swap_fee.unwrap_or(5u16), + pair_type: PairType::Stable {}, + is_disabled: false, + is_generator_disabled: false, + permissioned: false, + }], + token_code_id, + generator_address: None, + owner: owner.to_string(), + whitelist_code_id: 234u64, + coin_registry_address: coin_registry_address.to_string(), + }; + + let factory = app.instantiate_contract( + factory_code_id, + owner.clone(), + &init_msg, + &[], + "FACTORY", + None, + )?; + + let asset_infos = asset_infos_vec + .clone() + .into_iter() + .map(|(_, asset_info)| asset_info) + .collect_vec(); + let init_pair_msg = palomadex::factory::ExecuteMsg::CreatePair { + pair_type: PairType::Stable {}, + asset_infos: asset_infos.clone(), + init_params: Some(to_json_binary(&StablePoolParams { amp, owner: None }).unwrap()), + }; + + app.execute_contract(owner.clone(), factory.clone(), &init_pair_msg, &[])?; + + let resp: PairInfo = app.wrap().query_wasm_smart( + &factory, + &palomadex::factory::QueryMsg::Pair { asset_infos }, + )?; + + Ok(Self { + app, + owner: owner.clone(), + assets: asset_infos_vec.into_iter().collect(), + factory, + pair_addr: resp.contract_addr, + lp_token: resp.liquidity_token, + amp, + }) + } + + pub fn provide_liquidity(&mut self, sender: &Addr, assets: &[Asset]) -> AnyResult { + let funds = + assets.mock_coins_sent(&mut self.app, sender, &self.pair_addr, SendType::Allowance); + + let msg = ExecuteMsg::ProvideLiquidity { + assets: assets.to_vec(), + slippage_tolerance: None, + receiver: None, + }; + + self.app + .execute_contract(sender.clone(), self.pair_addr.clone(), &msg, &funds) + } + + pub fn withdraw_liquidity( + &mut self, + sender: &Addr, + amount: u128, + assets: Vec, + ) -> AnyResult { + let msg = Cw20ExecuteMsg::Send { + contract: self.pair_addr.to_string(), + amount: Uint128::from(amount), + msg: to_json_binary(&Cw20HookMsg::WithdrawLiquidity { assets }).unwrap(), + }; + + self.app + .execute_contract(sender.clone(), self.lp_token.clone(), &msg, &[]) + } + + pub fn swap( + &mut self, + sender: &Addr, + offer_asset: &Asset, + ask_asset_info: Option, + ) -> AnyResult { + match &offer_asset.info { + AssetInfo::Token { contract_addr } => { + let msg = Cw20ExecuteMsg::Send { + contract: self.pair_addr.to_string(), + amount: offer_asset.amount, + msg: to_json_binary(&Cw20HookMsg::Swap { + ask_asset_info, + belief_price: None, + max_spread: None, + to: None, + }) + .unwrap(), + }; + + self.app + .execute_contract(sender.clone(), contract_addr.clone(), &msg, &[]) + } + AssetInfo::NativeToken { .. } => { + let funds = offer_asset.mock_coin_sent( + &mut self.app, + sender, + &self.pair_addr, + SendType::None, + ); + + let msg = ExecuteMsg::Swap { + offer_asset: offer_asset.clone(), + ask_asset_info, + belief_price: None, + max_spread: None, + to: None, + }; + + self.app + .execute_contract(sender.clone(), self.pair_addr.clone(), &msg, &funds) + } + } + } + + pub fn simulate_swap( + &self, + offer_asset: &Asset, + ask_asset_info: Option, + ) -> StdResult { + self.app.wrap().query_wasm_smart( + &self.pair_addr, + &QueryMsg::Simulation { + offer_asset: offer_asset.clone(), + ask_asset_info, + }, + ) + } + + pub fn simulate_reverse_swap( + &self, + ask_asset: &Asset, + offer_asset_info: Option, + ) -> StdResult { + self.app.wrap().query_wasm_smart( + &self.pair_addr, + &QueryMsg::ReverseSimulation { + ask_asset: ask_asset.clone(), + offer_asset_info, + }, + ) + } + + pub fn query_prices(&self) -> StdResult { + self.app + .wrap() + .query_wasm_smart(&self.pair_addr, &QueryMsg::CumulativePrices {}) + } + + pub fn query_observe(&self, seconds_ago: u64) -> StdResult { + self.app + .wrap() + .query_wasm_smart(&self.pair_addr, &QueryMsg::Observe { seconds_ago }) + } + + fn init_token( + app: &mut App, + token_code: u64, + name: String, + decimals: u8, + owner: &Addr, + ) -> Addr { + let init_balance = INIT_BALANCE * 10u128.pow(decimals as u32); + app.instantiate_contract( + token_code, + owner.clone(), + &palomadex::token::InstantiateMsg { + symbol: name.to_string(), + name, + decimals, + initial_balances: vec![Cw20Coin { + address: owner.to_string(), + amount: Uint128::from(init_balance), + }], + mint: None, + marketing: None, + }, + &[], + "{name}_token", + None, + ) + .unwrap() + } + + pub fn token_balance(&self, token_addr: &Addr, user: &Addr) -> u128 { + let resp: BalanceResponse = self + .app + .wrap() + .query_wasm_smart( + token_addr, + &Cw20QueryMsg::Balance { + address: user.to_string(), + }, + ) + .unwrap(); + + resp.balance.u128() + } + + pub fn coin_balance(&self, coin: &TestCoin, user: &Addr) -> u128 { + match &self.assets[coin] { + AssetInfo::Token { contract_addr } => self.token_balance(contract_addr, user), + AssetInfo::NativeToken { denom } => self + .app + .wrap() + .query_balance(user, denom) + .unwrap() + .amount + .u128(), + } + } + + pub fn give_me_money(&mut self, assets: &[Asset], recipient: &Addr) { + let funds = + assets.mock_coins_sent(&mut self.app, &self.owner, recipient, SendType::Transfer); + + if !funds.is_empty() { + self.app + .send_tokens(self.owner.clone(), recipient.clone(), &funds) + .unwrap(); + } + } + + pub fn observe_price(&self, seconds_ago: u64) -> StdResult { + self.app + .wrap() + .query_wasm_smart::( + &self.pair_addr, + &QueryMsg::Observe { seconds_ago }, + ) + .map(|val| val.price) + } +} + +#[derive(Clone, Copy)] +pub enum SendType { + Allowance, + Transfer, + None, +} + +pub trait AssetExt { + fn mock_coin_sent( + &self, + app: &mut App, + user: &Addr, + spender: &Addr, + typ: SendType, + ) -> Vec; +} + +impl AssetExt for Asset { + fn mock_coin_sent( + &self, + app: &mut App, + user: &Addr, + spender: &Addr, + typ: SendType, + ) -> Vec { + let mut funds = vec![]; + match &self.info { + AssetInfo::Token { contract_addr } if !self.amount.is_zero() => { + let msg = match typ { + SendType::Allowance => Cw20ExecuteMsg::IncreaseAllowance { + spender: spender.to_string(), + amount: self.amount, + expires: None, + }, + SendType::Transfer => Cw20ExecuteMsg::Transfer { + recipient: spender.to_string(), + amount: self.amount, + }, + _ => unimplemented!(), + }; + app.execute_contract(user.clone(), contract_addr.clone(), &msg, &[]) + .unwrap(); + } + AssetInfo::NativeToken { denom } if !self.amount.is_zero() => { + funds = vec![coin(self.amount.u128(), denom)]; + } + _ => {} + } + + funds + } +} + +pub trait AssetsExt { + fn mock_coins_sent( + &self, + app: &mut App, + user: &Addr, + spender: &Addr, + typ: SendType, + ) -> Vec; +} + +impl AssetsExt for &[Asset] { + fn mock_coins_sent( + &self, + app: &mut App, + user: &Addr, + spender: &Addr, + typ: SendType, + ) -> Vec { + let mut funds = vec![]; + for asset in self.iter() { + funds.extend(asset.mock_coin_sent(app, user, spender, typ)); + } + funds + } +} + +pub trait AppExtension { + fn next_block(&mut self, time: u64); +} + +impl AppExtension for App { + fn next_block(&mut self, time: u64) { + self.update_block(|block| { + block.time = block.time.plus_seconds(time); + block.height += 1 + }); + } +} + +pub fn f64_to_dec(val: f64) -> T +where + T: FromStr, + T::Err: Error, +{ + T::from_str(&val.to_string()).unwrap() +} diff --git a/contracts/pair_stable/tests/integration.rs b/contracts/pair_stable/tests/integration.rs new file mode 100644 index 0000000..b68e4e6 --- /dev/null +++ b/contracts/pair_stable/tests/integration.rs @@ -0,0 +1,2130 @@ +#![cfg(not(tarpaulin_include))] + +use std::str::FromStr; + +use cosmwasm_std::{ + attr, from_json, to_json_binary, Addr, Coin, Decimal, QueryRequest, Uint128, WasmQuery, +}; +use cw20::{BalanceResponse, Cw20Coin, Cw20ExecuteMsg, Cw20QueryMsg, MinterResponse}; + +use palomadex::asset::{Asset, AssetInfo, PairInfo}; +use palomadex::factory::{ + ExecuteMsg as FactoryExecuteMsg, InstantiateMsg as FactoryInstantiateMsg, PairConfig, PairType, + QueryMsg as FactoryQueryMsg, +}; +use palomadex::observation::OracleObservation; +use palomadex::pair::{ + ConfigResponse, CumulativePricesResponse, Cw20HookMsg, ExecuteMsg, InstantiateMsg, + PoolResponse, QueryMsg, StablePoolConfig, StablePoolParams, StablePoolUpdateParams, + MAX_FEE_SHARE_BPS, TWAP_PRECISION, +}; +use palomadex::token::InstantiateMsg as TokenInstantiateMsg; +use palomadex_mocks::cw_multi_test::{App, ContractWrapper, Executor}; +use palomadex_pair_stable::error::ContractError; +use palomadex_pair_stable::math::{MAX_AMP, MAX_AMP_CHANGE, MIN_AMP_CHANGING_TIME}; + +const OWNER: &str = "owner"; + +fn mock_app(owner: Addr, coins: Vec) -> App { + App::new(|router, _, storage| { + // initialization moved to App construction + router.bank.init_balance(storage, &owner, coins).unwrap() + }) +} + +fn store_token_code(app: &mut App) -> u64 { + let astro_token_contract = Box::new(ContractWrapper::new_with_empty( + cw20_base::contract::execute, + cw20_base::contract::instantiate, + cw20_base::contract::query, + )); + + app.store_code(astro_token_contract) +} + +fn store_pair_code(app: &mut App) -> u64 { + let pair_contract = Box::new( + ContractWrapper::new_with_empty( + palomadex_pair_stable::contract::execute, + palomadex_pair_stable::contract::instantiate, + palomadex_pair_stable::contract::query, + ) + .with_reply_empty(palomadex_pair_stable::contract::reply), + ); + + app.store_code(pair_contract) +} + +fn store_factory_code(app: &mut App) -> u64 { + let factory_contract = Box::new( + ContractWrapper::new_with_empty( + palomadex_factory::contract::execute, + palomadex_factory::contract::instantiate, + palomadex_factory::contract::query, + ) + .with_reply_empty(palomadex_factory::contract::reply), + ); + + app.store_code(factory_contract) +} + +fn store_coin_registry_code(app: &mut App) -> u64 { + let coin_registry_contract = Box::new(ContractWrapper::new_with_empty( + palomadex_native_coin_registry::contract::execute, + palomadex_native_coin_registry::contract::instantiate, + palomadex_native_coin_registry::contract::query, + )); + + app.store_code(coin_registry_contract) +} + +fn instantiate_coin_registry(mut app: &mut App, coins: Option>) -> Addr { + let coin_registry_id = store_coin_registry_code(&mut app); + let coin_registry_address = app + .instantiate_contract( + coin_registry_id, + Addr::unchecked(OWNER), + &palomadex::native_coin_registry::InstantiateMsg { + owner: OWNER.to_string(), + }, + &[], + "Coin registry", + None, + ) + .unwrap(); + + if let Some(coins) = coins { + app.execute_contract( + Addr::unchecked(OWNER), + coin_registry_address.clone(), + &palomadex::native_coin_registry::ExecuteMsg::Add { + native_coins: coins, + }, + &[], + ) + .unwrap(); + } + + coin_registry_address +} + +fn instantiate_pair(mut router: &mut App, owner: &Addr) -> Addr { + let coin_registry_address = instantiate_coin_registry( + &mut router, + Some(vec![("uusd".to_string(), 6), ("uluna".to_string(), 6)]), + ); + + let token_contract_code_id = store_token_code(&mut router); + let pair_contract_code_id = store_pair_code(&mut router); + let factory_code_id = store_factory_code(&mut router); + + let factory_init_msg = FactoryInstantiateMsg { + fee_address: None, + pair_configs: vec![PairConfig { + code_id: pair_contract_code_id, + maker_fee_bps: 5000, + total_fee_bps: 5u16, + pair_type: PairType::Stable {}, + is_disabled: false, + is_generator_disabled: false, + permissioned: false, + }], + token_code_id: token_contract_code_id, + generator_address: None, + owner: owner.to_string(), + whitelist_code_id: 234u64, + coin_registry_address: coin_registry_address.to_string(), + }; + + let factory_addr = router + .instantiate_contract( + factory_code_id, + owner.clone(), + &factory_init_msg, + &[], + "FACTORY", + None, + ) + .unwrap(); + + let msg = InstantiateMsg { + asset_infos: vec![ + AssetInfo::NativeToken { + denom: "uusd".to_string(), + }, + AssetInfo::NativeToken { + denom: "uluna".to_string(), + }, + ], + token_code_id: token_contract_code_id, + factory_addr: factory_addr.to_string(), + init_params: None, + }; + + let resp = router + .instantiate_contract( + pair_contract_code_id, + owner.clone(), + &msg, + &[], + String::from("PAIR"), + None, + ) + .unwrap_err(); + assert_eq!( + "You need to provide init params", + resp.root_cause().to_string() + ); + + let msg = InstantiateMsg { + asset_infos: vec![ + AssetInfo::NativeToken { + denom: "uusd".to_string(), + }, + AssetInfo::NativeToken { + denom: "uluna".to_string(), + }, + ], + token_code_id: token_contract_code_id, + factory_addr: factory_addr.to_string(), + init_params: Some( + to_json_binary(&StablePoolParams { + amp: 100, + owner: None, + }) + .unwrap(), + ), + }; + + let pair = router + .instantiate_contract( + pair_contract_code_id, + owner.clone(), + &msg, + &[], + String::from("PAIR"), + None, + ) + .unwrap(); + + let res: PairInfo = router + .wrap() + .query_wasm_smart(pair.clone(), &QueryMsg::Pair {}) + .unwrap(); + assert_eq!("contract2", res.contract_addr); + assert_eq!("contract3", res.liquidity_token); + + pair +} + +#[test] +fn test_provide_and_withdraw_liquidity() { + let owner = Addr::unchecked("owner"); + let alice_address = Addr::unchecked("alice"); + + let mut router = mock_app( + owner.clone(), + vec![ + Coin { + denom: "uusd".to_string(), + amount: Uint128::new(100_000_000_000u128), + }, + Coin { + denom: "uluna".to_string(), + amount: Uint128::new(100_000_000_000u128), + }, + ], + ); + + // Set Alice's balances + router + .send_tokens( + owner.clone(), + alice_address.clone(), + &[ + Coin { + denom: "uusd".to_string(), + amount: Uint128::new(233_000u128), + }, + Coin { + denom: "uluna".to_string(), + amount: Uint128::new(200_000u128), + }, + ], + ) + .unwrap(); + + // Init pair + let pair_instance = instantiate_pair(&mut router, &owner); + + let res: Result = router.wrap().query(&QueryRequest::Wasm(WasmQuery::Smart { + contract_addr: pair_instance.to_string(), + msg: to_json_binary(&QueryMsg::Pair {}).unwrap(), + })); + let res = res.unwrap(); + + assert_eq!( + res.asset_infos, + [ + AssetInfo::NativeToken { + denom: "uusd".to_string(), + }, + AssetInfo::NativeToken { + denom: "uluna".to_string(), + }, + ], + ); + + // Try to provide liquidity less then MINIMUM_LIQUIDITY_AMOUNT + let (msg, coins) = provide_liquidity_msg(Uint128::new(100), Uint128::new(100), None); + let err = router + .execute_contract(alice_address.clone(), pair_instance.clone(), &msg, &coins) + .unwrap_err(); + assert_eq!( + "Initial liquidity must be more than 1000", + err.root_cause().to_string() + ); + + // Try to provide liquidity equal to MINIMUM_LIQUIDITY_AMOUNT + let (msg, coins) = provide_liquidity_msg(Uint128::new(500), Uint128::new(500), None); + let err = router + .execute_contract(alice_address.clone(), pair_instance.clone(), &msg, &coins) + .unwrap_err(); + assert_eq!( + "Initial liquidity must be more than 1000", + err.root_cause().to_string() + ); + + // Provide liquidity + let (msg, coins) = provide_liquidity_msg(Uint128::new(100000), Uint128::new(100000), None); + let res = router + .execute_contract(alice_address.clone(), pair_instance.clone(), &msg, &coins) + .unwrap(); + + assert_eq!( + res.events[1].attributes[1], + attr("action", "provide_liquidity") + ); + assert_eq!(res.events[1].attributes[3], attr("receiver", "alice"),); + assert_eq!( + res.events[1].attributes[4], + attr("assets", "100000uusd, 100000uluna") + ); + assert_eq!( + res.events[1].attributes[5], + attr("share", 199000u128.to_string()) + ); + + assert_eq!(res.events[3].attributes[1], attr("action", "mint")); + assert_eq!(res.events[3].attributes[2], attr("to", "contract2")); + assert_eq!( + res.events[3].attributes[3], + attr("amount", 1000.to_string()) + ); + + assert_eq!(res.events[5].attributes[1], attr("action", "mint")); + assert_eq!(res.events[5].attributes[2], attr("to", "alice")); + assert_eq!( + res.events[5].attributes[3], + attr("amount", 199000u128.to_string()) + ); + + // Provide liquidity for a custom receiver + let (msg, coins) = provide_liquidity_msg( + Uint128::new(100000), + Uint128::new(100000), + Some("bob".to_string()), + ); + let res = router + .execute_contract(alice_address.clone(), pair_instance.clone(), &msg, &coins) + .unwrap(); + + assert_eq!( + res.events[1].attributes[1], + attr("action", "provide_liquidity") + ); + assert_eq!(res.events[1].attributes[3], attr("receiver", "bob"),); + assert_eq!( + res.events[1].attributes[4], + attr("assets", "100000uusd, 100000uluna") + ); + assert_eq!( + res.events[1].attributes[5], + attr("share", 200000u128.to_string()) + ); + assert_eq!(res.events[3].attributes[1], attr("action", "mint")); + assert_eq!(res.events[3].attributes[2], attr("to", "bob")); + assert_eq!( + res.events[3].attributes[3], + attr("amount", 200000.to_string()) + ); +} + +fn provide_liquidity_msg( + uusd_amount: Uint128, + uluna_amount: Uint128, + receiver: Option, +) -> (ExecuteMsg, [Coin; 2]) { + let msg = ExecuteMsg::ProvideLiquidity { + assets: vec![ + Asset { + info: AssetInfo::NativeToken { + denom: "uusd".to_string(), + }, + amount: uusd_amount.clone(), + }, + Asset { + info: AssetInfo::NativeToken { + denom: "uluna".to_string(), + }, + amount: uluna_amount.clone(), + }, + ], + slippage_tolerance: None, + receiver, + }; + + let coins = [ + Coin { + denom: "uluna".to_string(), + amount: uluna_amount.clone(), + }, + Coin { + denom: "uusd".to_string(), + amount: uusd_amount.clone(), + }, + ]; + + (msg, coins) +} + +#[test] +fn provide_lp_for_single_token() { + let owner = Addr::unchecked(OWNER); + let mut app = mock_app( + owner.clone(), + vec![ + Coin { + denom: "uusd".to_string(), + amount: Uint128::new(100_000_000_000u128), + }, + Coin { + denom: "uluna".to_string(), + amount: Uint128::new(100_000_000_000u128), + }, + ], + ); + + let token_code_id = store_token_code(&mut app); + + let x_amount = Uint128::new(9_000_000_000_000_000); + let y_amount = Uint128::new(9_000_000_000_000_000); + let x_offer = Uint128::new(1_000_000_000_000_000); + let swap_amount = Uint128::new(120_000_000); + + let token_name = "Xtoken"; + + let init_msg = TokenInstantiateMsg { + name: token_name.to_string(), + symbol: token_name.to_string(), + decimals: 6, + initial_balances: vec![Cw20Coin { + address: OWNER.to_string(), + amount: x_amount, + }], + mint: Some(MinterResponse { + minter: String::from(OWNER), + cap: None, + }), + marketing: None, + }; + + let token_x_instance = app + .instantiate_contract( + token_code_id, + owner.clone(), + &init_msg, + &[], + token_name, + None, + ) + .unwrap(); + + let token_name = "Ytoken"; + + let init_msg = TokenInstantiateMsg { + name: token_name.to_string(), + symbol: token_name.to_string(), + decimals: 6, + initial_balances: vec![Cw20Coin { + address: OWNER.to_string(), + amount: y_amount, + }], + mint: Some(MinterResponse { + minter: String::from(OWNER), + cap: None, + }), + marketing: None, + }; + + let token_y_instance = app + .instantiate_contract( + token_code_id, + owner.clone(), + &init_msg, + &[], + token_name, + None, + ) + .unwrap(); + + let pair_code_id = store_pair_code(&mut app); + let factory_code_id = store_factory_code(&mut app); + + let init_msg = FactoryInstantiateMsg { + fee_address: None, + pair_configs: vec![PairConfig { + code_id: pair_code_id, + maker_fee_bps: 0, + total_fee_bps: 0, + pair_type: PairType::Stable {}, + is_disabled: false, + is_generator_disabled: false, + permissioned: false, + }], + token_code_id, + generator_address: Some(String::from("generator")), + owner: String::from("owner0000"), + whitelist_code_id: 234u64, + coin_registry_address: "coin_registry".to_string(), + }; + + let factory_instance = app + .instantiate_contract( + factory_code_id, + owner.clone(), + &init_msg, + &[], + "FACTORY", + None, + ) + .unwrap(); + + let msg = FactoryExecuteMsg::CreatePair { + pair_type: PairType::Stable {}, + asset_infos: vec![ + AssetInfo::Token { + contract_addr: token_x_instance.clone(), + }, + AssetInfo::Token { + contract_addr: token_y_instance.clone(), + }, + ], + init_params: Some( + to_json_binary(&StablePoolParams { + amp: 100, + owner: None, + }) + .unwrap(), + ), + }; + + app.execute_contract(owner.clone(), factory_instance.clone(), &msg, &[]) + .unwrap(); + + let msg = FactoryQueryMsg::Pair { + asset_infos: vec![ + AssetInfo::Token { + contract_addr: token_x_instance.clone(), + }, + AssetInfo::Token { + contract_addr: token_y_instance.clone(), + }, + ], + }; + + let res: PairInfo = app + .wrap() + .query_wasm_smart(&factory_instance, &msg) + .unwrap(); + + let pair_instance = res.contract_addr; + + let msg = Cw20ExecuteMsg::IncreaseAllowance { + spender: pair_instance.to_string(), + expires: None, + amount: x_amount, + }; + + app.execute_contract(owner.clone(), token_x_instance.clone(), &msg, &[]) + .unwrap(); + + let msg = Cw20ExecuteMsg::IncreaseAllowance { + spender: pair_instance.to_string(), + expires: None, + amount: y_amount, + }; + + app.execute_contract(owner.clone(), token_y_instance.clone(), &msg, &[]) + .unwrap(); + + let swap_msg = Cw20ExecuteMsg::Send { + contract: pair_instance.to_string(), + msg: to_json_binary(&Cw20HookMsg::Swap { + ask_asset_info: None, + belief_price: None, + max_spread: None, + to: None, + }) + .unwrap(), + amount: swap_amount, + }; + + let err = app + .execute_contract(owner.clone(), token_x_instance.clone(), &swap_msg, &[]) + .unwrap_err(); + assert_eq!( + "Generic error: One of the pools is empty", + err.root_cause().to_string() + ); + + let msg = ExecuteMsg::ProvideLiquidity { + assets: vec![ + Asset { + info: AssetInfo::Token { + contract_addr: token_x_instance.clone(), + }, + amount: x_offer, + }, + Asset { + info: AssetInfo::Token { + contract_addr: token_y_instance.clone(), + }, + amount: Uint128::zero(), + }, + ], + slippage_tolerance: None, + receiver: None, + }; + + let err = app + .execute_contract(owner.clone(), pair_instance.clone(), &msg, &[]) + .unwrap_err(); + assert_eq!( + "It is not possible to provide liquidity with one token for an empty pool", + err.root_cause().to_string() + ); + + let msg = ExecuteMsg::ProvideLiquidity { + assets: vec![ + Asset { + info: AssetInfo::Token { + contract_addr: token_x_instance.clone(), + }, + amount: Uint128::new(1_000_000_000_000_000), + }, + Asset { + info: AssetInfo::Token { + contract_addr: token_y_instance.clone(), + }, + amount: Uint128::new(1_000_000_000_000_000), + }, + ], + slippage_tolerance: None, + receiver: None, + }; + + app.execute_contract(owner.clone(), pair_instance.clone(), &msg, &[]) + .unwrap(); + + // try to provide for single token and increase the ratio in the pool from 1 to 1.5 + let msg = ExecuteMsg::ProvideLiquidity { + assets: vec![ + Asset { + info: AssetInfo::Token { + contract_addr: token_x_instance.clone(), + }, + amount: Uint128::new(500_000_000_000_000), + }, + Asset { + info: AssetInfo::Token { + contract_addr: token_y_instance.clone(), + }, + amount: Uint128::zero(), + }, + ], + slippage_tolerance: None, + receiver: None, + }; + + app.execute_contract(owner.clone(), pair_instance.clone(), &msg, &[]) + .unwrap(); + + // try swap 120_000_000 from token_y to token_x (from lower token amount to higher) + app.execute_contract(owner.clone(), token_y_instance.clone(), &swap_msg, &[]) + .unwrap(); + + // try swap 120_000_000 from token_x to token_y (from higher token amount to lower ) + app.execute_contract(owner.clone(), token_x_instance.clone(), &swap_msg, &[]) + .unwrap(); + + // try to provide for single token and increase the ratio in the pool from 1 to 2.5 + let msg = ExecuteMsg::ProvideLiquidity { + assets: vec![ + Asset { + info: AssetInfo::Token { + contract_addr: token_x_instance.clone(), + }, + amount: Uint128::new(1_000_000_000_000_000), + }, + Asset { + info: AssetInfo::Token { + contract_addr: token_y_instance.clone(), + }, + amount: Uint128::zero(), + }, + ], + slippage_tolerance: None, + receiver: None, + }; + + app.execute_contract(owner.clone(), pair_instance.clone(), &msg, &[]) + .unwrap(); + + // try swap 120_000_000 from token_y to token_x (from lower token amount to higher) + let msg = Cw20ExecuteMsg::Send { + contract: pair_instance.to_string(), + msg: to_json_binary(&Cw20HookMsg::Swap { + ask_asset_info: None, + belief_price: None, + max_spread: None, + to: None, + }) + .unwrap(), + amount: swap_amount, + }; + + app.execute_contract(owner.clone(), token_y_instance.clone(), &msg, &[]) + .unwrap(); + + // try swap 120_000_000 from token_x to token_y (from higher token amount to lower ) + let msg = Cw20ExecuteMsg::Send { + contract: pair_instance.to_string(), + msg: to_json_binary(&Cw20HookMsg::Swap { + ask_asset_info: None, + belief_price: None, + max_spread: None, + to: None, + }) + .unwrap(), + amount: swap_amount, + }; + + let err = app + .execute_contract(owner.clone(), token_x_instance.clone(), &msg, &[]) + .unwrap_err(); + assert_eq!( + err.root_cause().to_string(), + "Operation exceeds max spread limit" + ); +} + +#[test] +fn test_compatibility_of_tokens_with_different_precision() { + let owner = Addr::unchecked(OWNER); + let mut app = mock_app( + owner.clone(), + vec![ + Coin { + denom: "uusd".to_string(), + amount: Uint128::new(100_000_000_000u128), + }, + Coin { + denom: "uluna".to_string(), + amount: Uint128::new(100_000_000_000u128), + }, + ], + ); + + let token_code_id = store_token_code(&mut app); + + let x_amount = Uint128::new(1000000_00000); + let y_amount = Uint128::new(1000000_0000000); + let x_offer = Uint128::new(1_00000); + let y_expected_return = Uint128::new(1_0000000); + + let token_name = "Xtoken"; + + let init_msg = TokenInstantiateMsg { + name: token_name.to_string(), + symbol: token_name.to_string(), + decimals: 5, + initial_balances: vec![Cw20Coin { + address: OWNER.to_string(), + amount: x_amount + x_offer, + }], + mint: Some(MinterResponse { + minter: String::from(OWNER), + cap: None, + }), + marketing: None, + }; + + let token_x_instance = app + .instantiate_contract( + token_code_id, + owner.clone(), + &init_msg, + &[], + token_name, + None, + ) + .unwrap(); + + let token_name = "Ytoken"; + + let init_msg = TokenInstantiateMsg { + name: token_name.to_string(), + symbol: token_name.to_string(), + decimals: 7, + initial_balances: vec![Cw20Coin { + address: OWNER.to_string(), + amount: y_amount, + }], + mint: Some(MinterResponse { + minter: String::from(OWNER), + cap: None, + }), + marketing: None, + }; + + let token_y_instance = app + .instantiate_contract( + token_code_id, + owner.clone(), + &init_msg, + &[], + token_name, + None, + ) + .unwrap(); + + let pair_code_id = store_pair_code(&mut app); + let factory_code_id = store_factory_code(&mut app); + + let init_msg = FactoryInstantiateMsg { + fee_address: None, + pair_configs: vec![PairConfig { + code_id: pair_code_id, + maker_fee_bps: 0, + total_fee_bps: 0, + pair_type: PairType::Stable {}, + is_disabled: false, + is_generator_disabled: false, + permissioned: false, + }], + token_code_id, + generator_address: Some(String::from("generator")), + owner: String::from("owner0000"), + whitelist_code_id: 234u64, + coin_registry_address: "coin_registry".to_string(), + }; + + let factory_instance = app + .instantiate_contract( + factory_code_id, + owner.clone(), + &init_msg, + &[], + "FACTORY", + None, + ) + .unwrap(); + + let msg = FactoryExecuteMsg::CreatePair { + pair_type: PairType::Stable {}, + asset_infos: vec![ + AssetInfo::Token { + contract_addr: token_x_instance.clone(), + }, + AssetInfo::Token { + contract_addr: token_y_instance.clone(), + }, + ], + init_params: Some( + to_json_binary(&StablePoolParams { + amp: 100, + owner: None, + }) + .unwrap(), + ), + }; + + app.execute_contract(owner.clone(), factory_instance.clone(), &msg, &[]) + .unwrap(); + + let msg = FactoryQueryMsg::Pair { + asset_infos: vec![ + AssetInfo::Token { + contract_addr: token_x_instance.clone(), + }, + AssetInfo::Token { + contract_addr: token_y_instance.clone(), + }, + ], + }; + + let res: PairInfo = app + .wrap() + .query_wasm_smart(&factory_instance, &msg) + .unwrap(); + + let pair_instance = res.contract_addr; + + let msg = Cw20ExecuteMsg::IncreaseAllowance { + spender: pair_instance.to_string(), + expires: None, + amount: x_amount + x_offer, + }; + + app.execute_contract(owner.clone(), token_x_instance.clone(), &msg, &[]) + .unwrap(); + + let msg = Cw20ExecuteMsg::IncreaseAllowance { + spender: pair_instance.to_string(), + expires: None, + amount: y_amount, + }; + + app.execute_contract(owner.clone(), token_y_instance.clone(), &msg, &[]) + .unwrap(); + + let msg = ExecuteMsg::ProvideLiquidity { + assets: vec![ + Asset { + info: AssetInfo::Token { + contract_addr: token_x_instance.clone(), + }, + amount: x_amount, + }, + Asset { + info: AssetInfo::Token { + contract_addr: token_y_instance.clone(), + }, + amount: y_amount, + }, + ], + slippage_tolerance: None, + receiver: None, + }; + + app.execute_contract(owner.clone(), pair_instance.clone(), &msg, &[]) + .unwrap(); + + let d: u128 = app + .wrap() + .query_wasm_smart(&pair_instance, &QueryMsg::QueryComputeD {}) + .unwrap(); + assert_eq!(d, 20000000000000); + + let user = Addr::unchecked("user"); + + let msg = Cw20ExecuteMsg::Send { + contract: pair_instance.to_string(), + msg: to_json_binary(&Cw20HookMsg::Swap { + ask_asset_info: None, + belief_price: None, + max_spread: None, + to: Some(user.to_string()), + }) + .unwrap(), + amount: x_offer, + }; + + app.execute_contract(owner.clone(), token_x_instance.clone(), &msg, &[]) + .unwrap(); + + let msg = Cw20QueryMsg::Balance { + address: user.to_string(), + }; + + let res: BalanceResponse = app + .wrap() + .query_wasm_smart(&token_y_instance, &msg) + .unwrap(); + + assert_eq!(res.balance, y_expected_return); + + let d: u128 = app + .wrap() + .query_wasm_smart(&pair_instance, &QueryMsg::QueryComputeD {}) + .unwrap(); + assert_eq!(d, 19999999999999); +} + +#[test] +fn test_if_twap_is_calculated_correctly_when_pool_idles() { + let owner = Addr::unchecked(OWNER); + let mut app = mock_app( + owner.clone(), + vec![ + Coin { + denom: "uusd".to_string(), + amount: Uint128::new(100_000_000_000000u128), + }, + Coin { + denom: "uluna".to_string(), + amount: Uint128::new(100_000_000_000000u128), + }, + ], + ); + + let user1 = Addr::unchecked("user1"); + + // Set User1's balances + app.send_tokens( + owner.clone(), + user1.clone(), + &[ + Coin { + denom: "uusd".to_string(), + amount: Uint128::new(4666666_000000), + }, + Coin { + denom: "uluna".to_string(), + amount: Uint128::new(2000000_000000), + }, + ], + ) + .unwrap(); + + // Instantiate pair + let pair_instance = instantiate_pair(&mut app, &user1); + + // Provide liquidity, accumulators are empty + let (msg, coins) = provide_liquidity_msg( + Uint128::new(1000000_000000), + Uint128::new(1000000_000000), + None, + ); + app.execute_contract(user1.clone(), pair_instance.clone(), &msg, &coins) + .unwrap(); + + const BLOCKS_PER_DAY: u64 = 17280; + const ELAPSED_SECONDS: u64 = BLOCKS_PER_DAY * 5; + + // A day later + app.update_block(|b| { + b.height += BLOCKS_PER_DAY; + b.time = b.time.plus_seconds(ELAPSED_SECONDS); + }); + + // Provide liquidity, accumulators firstly filled with the same prices + let (msg, coins) = provide_liquidity_msg( + Uint128::new(3000000_000000), + Uint128::new(1000000_000000), + None, + ); + app.execute_contract(user1.clone(), pair_instance.clone(), &msg, &coins) + .unwrap(); + + // Get current TWAP accumulator values + let msg = QueryMsg::CumulativePrices {}; + let cpr_old: CumulativePricesResponse = + app.wrap().query_wasm_smart(&pair_instance, &msg).unwrap(); + + // A day later + app.update_block(|b| { + b.height += BLOCKS_PER_DAY; + b.time = b.time.plus_seconds(ELAPSED_SECONDS); + }); + + // Get current twap accumulator values + let msg = QueryMsg::CumulativePrices {}; + let cpr_new: CumulativePricesResponse = + app.wrap().query_wasm_smart(&pair_instance, &msg).unwrap(); + + let twap0 = cpr_new.cumulative_prices[0].2 - cpr_old.cumulative_prices[0].2; + let twap1 = cpr_new.cumulative_prices[1].2 - cpr_old.cumulative_prices[1].2; + + // Prices weren't changed for the last day, uusd amount in pool = 4000000_000000, uluna = 2000000_000000 + let price_precision = Uint128::from(10u128.pow(TWAP_PRECISION.into())); + assert_eq!(twap0 / price_precision, Uint128::new(85684)); // 1.008356286 * ELAPSED_SECONDS (86400) + assert_eq!(twap1 / price_precision, Uint128::new(87121)); // 0.991712963 * ELAPSED_SECONDS +} + +#[test] +fn create_pair_with_same_assets() { + let owner = Addr::unchecked(OWNER); + let mut router = mock_app( + owner.clone(), + vec![ + Coin { + denom: "uusd".to_string(), + amount: Uint128::new(100_000_000_000u128), + }, + Coin { + denom: "uluna".to_string(), + amount: Uint128::new(100_000_000_000u128), + }, + ], + ); + + let token_contract_code_id = store_token_code(&mut router); + let pair_contract_code_id = store_pair_code(&mut router); + + let msg = InstantiateMsg { + asset_infos: vec![ + AssetInfo::NativeToken { + denom: "uusd".to_string(), + }, + AssetInfo::NativeToken { + denom: "uusd".to_string(), + }, + ], + token_code_id: token_contract_code_id, + factory_addr: String::from("factory"), + init_params: None, + }; + + let resp = router + .instantiate_contract( + pair_contract_code_id, + owner.clone(), + &msg, + &[], + String::from("PAIR"), + None, + ) + .unwrap_err(); + + assert_eq!( + resp.root_cause().to_string(), + "Doubling assets in asset infos" + ) +} + +#[test] +fn update_pair_config() { + let owner = Addr::unchecked(OWNER); + let mut router = mock_app( + owner.clone(), + vec![ + Coin { + denom: "uusd".to_string(), + amount: Uint128::new(100_000_000_000u128), + }, + Coin { + denom: "uluna".to_string(), + amount: Uint128::new(100_000_000_000u128), + }, + ], + ); + + let coin_registry_address = instantiate_coin_registry( + &mut router, + Some(vec![("uusd".to_string(), 6), ("uluna".to_string(), 6)]), + ); + + let token_contract_code_id = store_token_code(&mut router); + let pair_contract_code_id = store_pair_code(&mut router); + + let factory_code_id = store_factory_code(&mut router); + + let init_msg = FactoryInstantiateMsg { + fee_address: None, + pair_configs: vec![], + token_code_id: token_contract_code_id, + generator_address: Some(String::from("generator")), + owner: owner.to_string(), + whitelist_code_id: 234u64, + coin_registry_address: coin_registry_address.to_string(), + }; + + let factory_instance = router + .instantiate_contract( + factory_code_id, + owner.clone(), + &init_msg, + &[], + "FACTORY", + None, + ) + .unwrap(); + + let msg = InstantiateMsg { + asset_infos: vec![ + AssetInfo::NativeToken { + denom: "uusd".to_string(), + }, + AssetInfo::NativeToken { + denom: "uluna".to_string(), + }, + ], + token_code_id: token_contract_code_id, + factory_addr: factory_instance.to_string(), + init_params: Some( + to_json_binary(&StablePoolParams { + amp: 100, + owner: None, + }) + .unwrap(), + ), + }; + + let pair = router + .instantiate_contract( + pair_contract_code_id, + owner.clone(), + &msg, + &[], + String::from("PAIR"), + None, + ) + .unwrap(); + + let res: ConfigResponse = router + .wrap() + .query_wasm_smart(pair.clone(), &QueryMsg::Config {}) + .unwrap(); + + let params: StablePoolConfig = from_json(&res.params.unwrap()).unwrap(); + + assert_eq!(params.amp, Decimal::from_ratio(100u32, 1u32)); + + // Start changing amp with incorrect next amp + let msg = ExecuteMsg::UpdateConfig { + params: to_json_binary(&StablePoolUpdateParams::StartChangingAmp { + next_amp: MAX_AMP + 1, + next_amp_time: router.block_info().time.seconds(), + }) + .unwrap(), + }; + + let resp = router + .execute_contract(owner.clone(), pair.clone(), &msg, &[]) + .unwrap_err(); + + assert_eq!( + resp.root_cause().to_string(), + format!( + "Amp coefficient must be greater than 0 and less than or equal to {}", + MAX_AMP + ) + ); + + // Start changing amp with big difference between the old and new amp value + let msg = ExecuteMsg::UpdateConfig { + params: to_json_binary(&StablePoolUpdateParams::StartChangingAmp { + next_amp: 100 * MAX_AMP_CHANGE + 1, + next_amp_time: router.block_info().time.seconds(), + }) + .unwrap(), + }; + + let resp = router + .execute_contract(owner.clone(), pair.clone(), &msg, &[]) + .unwrap_err(); + + assert_eq!( + resp.root_cause().to_string(), + format!( + "The difference between the old and new amp value must not exceed {} times", + MAX_AMP_CHANGE + ) + ); + + // Start changing amp before the MIN_AMP_CHANGING_TIME has elapsed + let msg = ExecuteMsg::UpdateConfig { + params: to_json_binary(&StablePoolUpdateParams::StartChangingAmp { + next_amp: 250, + next_amp_time: router.block_info().time.seconds(), + }) + .unwrap(), + }; + + let resp = router + .execute_contract(owner.clone(), pair.clone(), &msg, &[]) + .unwrap_err(); + + assert_eq!( + resp.root_cause().to_string(), + format!( + "Amp coefficient cannot be changed more often than once per {} seconds", + MIN_AMP_CHANGING_TIME + ) + ); + + // Start increasing amp + router.update_block(|b| { + b.time = b.time.plus_seconds(MIN_AMP_CHANGING_TIME); + }); + + let msg = ExecuteMsg::UpdateConfig { + params: to_json_binary(&StablePoolUpdateParams::StartChangingAmp { + next_amp: 250, + next_amp_time: router.block_info().time.seconds() + MIN_AMP_CHANGING_TIME, + }) + .unwrap(), + }; + + router + .execute_contract(owner.clone(), pair.clone(), &msg, &[]) + .unwrap(); + + router.update_block(|b| { + b.time = b.time.plus_seconds(MIN_AMP_CHANGING_TIME / 2); + }); + + let res: ConfigResponse = router + .wrap() + .query_wasm_smart(pair.clone(), &QueryMsg::Config {}) + .unwrap(); + + let params: StablePoolConfig = from_json(&res.params.unwrap()).unwrap(); + + assert_eq!(params.amp, Decimal::from_ratio(175u32, 1u32)); + + router.update_block(|b| { + b.time = b.time.plus_seconds(MIN_AMP_CHANGING_TIME / 2); + }); + + let res: ConfigResponse = router + .wrap() + .query_wasm_smart(pair.clone(), &QueryMsg::Config {}) + .unwrap(); + + let params: StablePoolConfig = from_json(&res.params.unwrap()).unwrap(); + + assert_eq!(params.amp, Decimal::from_ratio(250u32, 1u32)); + + // Start decreasing amp + router.update_block(|b| { + b.time = b.time.plus_seconds(MIN_AMP_CHANGING_TIME); + }); + + let msg = ExecuteMsg::UpdateConfig { + params: to_json_binary(&StablePoolUpdateParams::StartChangingAmp { + next_amp: 50, + next_amp_time: router.block_info().time.seconds() + MIN_AMP_CHANGING_TIME, + }) + .unwrap(), + }; + + router + .execute_contract(owner.clone(), pair.clone(), &msg, &[]) + .unwrap(); + + router.update_block(|b| { + b.time = b.time.plus_seconds(MIN_AMP_CHANGING_TIME / 2); + }); + + let res: ConfigResponse = router + .wrap() + .query_wasm_smart(pair.clone(), &QueryMsg::Config {}) + .unwrap(); + + let params: StablePoolConfig = from_json(&res.params.unwrap()).unwrap(); + + assert_eq!(params.amp, Decimal::from_ratio(150u32, 1u32)); + + // Stop changing amp + let msg = ExecuteMsg::UpdateConfig { + params: to_json_binary(&StablePoolUpdateParams::StopChangingAmp {}).unwrap(), + }; + + router + .execute_contract(owner.clone(), pair.clone(), &msg, &[]) + .unwrap(); + + router.update_block(|b| { + b.time = b.time.plus_seconds(MIN_AMP_CHANGING_TIME / 2); + }); + + let res: ConfigResponse = router + .wrap() + .query_wasm_smart(pair.clone(), &QueryMsg::Config {}) + .unwrap(); + + let params: StablePoolConfig = from_json(&res.params.unwrap()).unwrap(); + + assert_eq!(params.amp, Decimal::from_ratio(150u32, 1u32)); +} + +#[test] +fn enable_disable_fee_sharing() { + let owner = Addr::unchecked(OWNER); + let mut router = mock_app( + owner.clone(), + vec![ + Coin { + denom: "uusd".to_string(), + amount: Uint128::new(100_000_000_000u128), + }, + Coin { + denom: "uluna".to_string(), + amount: Uint128::new(100_000_000_000u128), + }, + ], + ); + + let coin_registry_address = instantiate_coin_registry( + &mut router, + Some(vec![("uusd".to_string(), 6), ("uluna".to_string(), 6)]), + ); + + let token_contract_code_id = store_token_code(&mut router); + let pair_contract_code_id = store_pair_code(&mut router); + + let factory_code_id = store_factory_code(&mut router); + + let init_msg = FactoryInstantiateMsg { + fee_address: None, + pair_configs: vec![], + token_code_id: token_contract_code_id, + generator_address: Some(String::from("generator")), + owner: owner.to_string(), + whitelist_code_id: 234u64, + coin_registry_address: coin_registry_address.to_string(), + }; + + let factory_instance = router + .instantiate_contract( + factory_code_id, + owner.clone(), + &init_msg, + &[], + "FACTORY", + None, + ) + .unwrap(); + + let msg = InstantiateMsg { + asset_infos: vec![ + AssetInfo::NativeToken { + denom: "uusd".to_string(), + }, + AssetInfo::NativeToken { + denom: "uluna".to_string(), + }, + ], + token_code_id: token_contract_code_id, + factory_addr: factory_instance.to_string(), + init_params: Some( + to_json_binary(&StablePoolParams { + amp: 100, + owner: None, + }) + .unwrap(), + ), + }; + + let pair = router + .instantiate_contract( + pair_contract_code_id, + owner.clone(), + &msg, + &[], + String::from("PAIR"), + None, + ) + .unwrap(); + + let res: ConfigResponse = router + .wrap() + .query_wasm_smart(pair.clone(), &QueryMsg::Config {}) + .unwrap(); + + let params: StablePoolConfig = from_json(&res.params.unwrap()).unwrap(); + + assert_eq!(params.amp, Decimal::from_ratio(100u32, 1u32)); + assert_eq!(params.fee_share, None); + + // Attemt to set fee sharing higher than maximum + let msg = ExecuteMsg::UpdateConfig { + params: to_json_binary(&StablePoolUpdateParams::EnableFeeShare { + fee_share_bps: MAX_FEE_SHARE_BPS + 1, + fee_share_address: "contract".to_string(), + }) + .unwrap(), + }; + + assert_eq!( + router + .execute_contract(owner.clone(), pair.clone(), &msg, &[]) + .unwrap_err() + .downcast_ref::() + .unwrap(), + &ContractError::FeeShareOutOfBounds {} + ); + + // Attemt to set fee sharing to 0 + let msg = ExecuteMsg::UpdateConfig { + params: to_json_binary(&StablePoolUpdateParams::EnableFeeShare { + fee_share_bps: 0, + fee_share_address: "contract".to_string(), + }) + .unwrap(), + }; + + assert_eq!( + router + .execute_contract(owner.clone(), pair.clone(), &msg, &[]) + .unwrap_err() + .downcast_ref::() + .unwrap(), + &ContractError::FeeShareOutOfBounds {} + ); + + let fee_share_bps = 500; // 5% + let fee_share_address = "contract".to_string(); + + // Set valid fee share + let msg = ExecuteMsg::UpdateConfig { + params: to_json_binary(&StablePoolUpdateParams::EnableFeeShare { + fee_share_bps, + fee_share_address: fee_share_address.clone(), + }) + .unwrap(), + }; + + router + .execute_contract(owner.clone(), pair.clone(), &msg, &[]) + .unwrap(); + + let res: ConfigResponse = router + .wrap() + .query_wasm_smart(pair.clone(), &QueryMsg::Config {}) + .unwrap(); + + let params: StablePoolConfig = from_json(&res.params.unwrap()).unwrap(); + + let set_fee_share = params.fee_share.unwrap(); + assert_eq!(set_fee_share.bps, fee_share_bps); + assert_eq!(set_fee_share.recipient, fee_share_address); + + // Disable fee share + let msg = ExecuteMsg::UpdateConfig { + params: to_json_binary(&StablePoolUpdateParams::DisableFeeShare {}).unwrap(), + }; + + router + .execute_contract(owner.clone(), pair.clone(), &msg, &[]) + .unwrap(); + + let res: ConfigResponse = router + .wrap() + .query_wasm_smart(pair.clone(), &QueryMsg::Config {}) + .unwrap(); + + let params: StablePoolConfig = from_json(&res.params.unwrap()).unwrap(); + assert!(params.fee_share.is_none()); +} + +#[test] +fn check_observe_queries() { + let owner = Addr::unchecked("owner"); + let user1 = Addr::unchecked("user1"); + + let mut app = mock_app( + owner.clone(), + vec![ + Coin { + denom: "uusd".to_string(), + amount: Uint128::new(100_000_000_000000u128), + }, + Coin { + denom: "uluna".to_string(), + amount: Uint128::new(100_000_000_000000u128), + }, + ], + ); + + // Set Alice's balances + app.send_tokens( + owner.clone(), + user1.clone(), + &[ + Coin { + denom: "uusd".to_string(), + amount: Uint128::new(4000000_000000), + }, + Coin { + denom: "uluna".to_string(), + amount: Uint128::new(2000000_000000), + }, + ], + ) + .unwrap(); + + // Instantiate pair + let pair_instance = instantiate_pair(&mut app, &user1); + + // Provide liquidity + let (msg, coins) = provide_liquidity_msg( + Uint128::new(1000000_000000), + Uint128::new(1000000_000000), + None, + ); + app.execute_contract(user1.clone(), pair_instance.clone(), &msg, &coins) + .unwrap(); + + // swap + let msg = ExecuteMsg::Swap { + offer_asset: Asset { + info: AssetInfo::NativeToken { + denom: "uusd".to_owned(), + }, + amount: Uint128::new(100_000000), + }, + ask_asset_info: None, + belief_price: None, + max_spread: None, + to: None, + }; + let send_funds = vec![Coin { + denom: "uusd".to_owned(), + amount: Uint128::new(100_000000), + }]; + app.execute_contract(owner.clone(), pair_instance.clone(), &msg, &send_funds) + .unwrap(); + + app.update_block(|b| { + b.height += 1; + b.time = b.time.plus_seconds(1000); + }); + + let res: OracleObservation = app + .wrap() + .query_wasm_smart( + pair_instance.to_string(), + &QueryMsg::Observe { seconds_ago: 0 }, + ) + .unwrap(); + + assert_eq!( + res, + OracleObservation { + timestamp: app.block_info().time.seconds(), + price: Decimal::from_str("1.000501231106759864").unwrap() + } + ); +} + +#[test] +fn test_imbalance_withdraw_is_disabled() { + let owner = Addr::unchecked("owner"); + let alice_address = Addr::unchecked("alice"); + + let mut router = mock_app( + owner.clone(), + vec![ + Coin { + denom: "uusd".to_string(), + amount: Uint128::new(100_000_000_000u128), + }, + Coin { + denom: "uluna".to_string(), + amount: Uint128::new(100_000_000_000u128), + }, + Coin { + denom: "cny".to_string(), + amount: Uint128::new(100_000_000_000u128), + }, + ], + ); + + // Set Alice's balances + router + .send_tokens( + owner.clone(), + alice_address.clone(), + &[ + Coin { + denom: "uusd".to_string(), + amount: Uint128::new(233_000u128), + }, + Coin { + denom: "uluna".to_string(), + amount: Uint128::new(200_000u128), + }, + Coin { + denom: "cny".to_string(), + amount: Uint128::from(100_000_000u128), + }, + ], + ) + .unwrap(); + + // Init pair + let pair_instance = instantiate_pair(&mut router, &owner); + + let res: PairInfo = router + .wrap() + .query(&QueryRequest::Wasm(WasmQuery::Smart { + contract_addr: pair_instance.to_string(), + msg: to_json_binary(&QueryMsg::Pair {}).unwrap(), + })) + .unwrap(); + + let lp_token = res.liquidity_token; + + assert_eq!( + res.asset_infos, + [ + AssetInfo::NativeToken { + denom: "uusd".to_string(), + }, + AssetInfo::NativeToken { + denom: "uluna".to_string(), + }, + ], + ); + + // Provide liquidity + let (msg, coins) = provide_liquidity_msg(Uint128::new(100000), Uint128::new(100000), None); + router + .execute_contract(alice_address.clone(), pair_instance.clone(), &msg, &coins) + .unwrap(); + + // Provide liquidity for a custom receiver + let (msg, coins) = provide_liquidity_msg( + Uint128::new(100000), + Uint128::new(100000), + Some("bob".to_string()), + ); + router + .execute_contract(alice_address.clone(), pair_instance.clone(), &msg, &coins) + .unwrap(); + + // Check that imbalanced withdraw is currently disabled + let msg_imbalance = Cw20ExecuteMsg::Send { + contract: pair_instance.to_string(), + amount: Uint128::from(50u8), + msg: to_json_binary(&Cw20HookMsg::WithdrawLiquidity { + assets: vec![Asset { + info: AssetInfo::NativeToken { + denom: "uusd".to_string(), + }, + amount: Uint128::from(100u8), + }], + }) + .unwrap(), + }; + + let err = router + .execute_contract(alice_address.clone(), lp_token.clone(), &msg_imbalance, &[]) + .unwrap_err(); + assert_eq!( + err.root_cause().to_string(), + "Generic error: Imbalanced withdraw is currently disabled" + ); +} + +#[test] +fn test_provide_liquidity_without_funds() { + let owner = Addr::unchecked("owner"); + let alice_address = Addr::unchecked("alice"); + let mut router = mock_app( + owner.clone(), + vec![ + Coin { + denom: "uusd".to_string(), + amount: Uint128::new(100_000_000_000u128), + }, + Coin { + denom: "uluna".to_string(), + amount: Uint128::new(100_000_000_000u128), + }, + Coin { + denom: "cny".to_string(), + amount: Uint128::new(100_000_000_000u128), + }, + ], + ); + + // Set Alice's balances + router + .send_tokens( + owner.clone(), + alice_address.clone(), + &[ + Coin { + denom: "uusd".to_string(), + amount: Uint128::new(233_000_000u128), + }, + Coin { + denom: "uluna".to_string(), + amount: Uint128::new(2_00_000_000u128), + }, + Coin { + denom: "cny".to_string(), + amount: Uint128::from(100_000_000u128), + }, + ], + ) + .unwrap(); + + // Init pair + let pair_instance = instantiate_pair(&mut router, &owner); + + let res: PairInfo = router + .wrap() + .query_wasm_smart(pair_instance.to_string(), &QueryMsg::Pair {}) + .unwrap(); + + assert_eq!( + res.asset_infos, + [ + AssetInfo::NativeToken { + denom: "uusd".to_string(), + }, + AssetInfo::NativeToken { + denom: "uluna".to_string(), + }, + ], + ); + + // provide some liquidity to assume contract have funds (to prevent underflow err) + let (msg, coins) = + provide_liquidity_msg(Uint128::new(100_000_000), Uint128::new(100_000_000), None); + + router + .execute_contract(alice_address.clone(), pair_instance.clone(), &msg, &coins) + .unwrap(); + + router + .execute_contract(alice_address.clone(), pair_instance.clone(), &msg, &coins) + .unwrap(); + + // provide liquidity without funds + + let err = router + .execute_contract(alice_address.clone(), pair_instance.clone(), &msg, &[]) + .unwrap_err(); + + assert_eq!( + err.root_cause().to_string(), + "Generic error: Native token balance mismatch between the argument (100000000uusd) and the transferred (0uusd)" + ); +} + +#[test] +fn check_correct_fee_share() { + // Validate the resulting values + // We swapped 1_000000 of token X + // Fee is set to 0.05% of the swap amount resulting in 1000000 * 0.0005 = 500 + // User receives with 1000000 - 500 = 999500 + // Of the 500 fee, 10% is sent to the fee sharing contract resulting in 50 + + // Test with 10% fee share, 0.05% total fee and 50% maker fee + test_fee_share( + 5000u16, + 5u16, + 1000u16, + Uint128::from(50u64), + Uint128::from(225u64), + ); + + // Test with 5% fee share, 0.05% total fee and 50% maker fee + test_fee_share( + 5000u16, + 5u16, + 500u16, + Uint128::from(25u64), + Uint128::from(237u64), + ); + + // // Test with 5% fee share, 0.1% total fee and 33.33% maker fee + test_fee_share( + 3333u16, + 10u16, + 500u16, + Uint128::from(50u64), + Uint128::from(316u64), + ); +} + +fn test_fee_share( + maker_fee_bps: u16, + total_fee_bps: u16, + fee_share_bps: u16, + expected_fee_share: Uint128, + expected_maker_fee: Uint128, +) { + let owner = Addr::unchecked(OWNER); + let mut app = mock_app( + owner.clone(), + vec![ + Coin { + denom: "uusd".to_string(), + amount: Uint128::new(100_000_000_000u128), + }, + Coin { + denom: "uluna".to_string(), + amount: Uint128::new(100_000_000_000u128), + }, + ], + ); + + let token_code_id = store_token_code(&mut app); + + let x_amount = Uint128::new(1_000_000_000000); + let y_amount = Uint128::new(1_000_000_000000); + let x_offer = Uint128::new(1_000000); + + let token_name = "Xtoken"; + + let init_msg = TokenInstantiateMsg { + name: token_name.to_string(), + symbol: token_name.to_string(), + decimals: 6, + initial_balances: vec![Cw20Coin { + address: OWNER.to_string(), + amount: x_amount + x_offer, + }], + mint: Some(MinterResponse { + minter: String::from(OWNER), + cap: None, + }), + marketing: None, + }; + + let token_x_instance = app + .instantiate_contract( + token_code_id, + owner.clone(), + &init_msg, + &[], + token_name, + None, + ) + .unwrap(); + + let token_name = "Ytoken"; + + let init_msg = TokenInstantiateMsg { + name: token_name.to_string(), + symbol: token_name.to_string(), + decimals: 6, + initial_balances: vec![Cw20Coin { + address: OWNER.to_string(), + amount: y_amount, + }], + mint: Some(MinterResponse { + minter: String::from(OWNER), + cap: None, + }), + marketing: None, + }; + + let token_y_instance = app + .instantiate_contract( + token_code_id, + owner.clone(), + &init_msg, + &[], + token_name, + None, + ) + .unwrap(); + + let pair_code_id = store_pair_code(&mut app); + let factory_code_id = store_factory_code(&mut app); + + let maker_address = "maker".to_string(); + + let init_msg = FactoryInstantiateMsg { + fee_address: Some(maker_address.clone()), + pair_configs: vec![PairConfig { + code_id: pair_code_id, + maker_fee_bps, + total_fee_bps, + pair_type: PairType::Stable {}, + is_disabled: false, + is_generator_disabled: false, + permissioned: false, + }], + token_code_id, + generator_address: Some(String::from("generator")), + owner: String::from("owner0000"), + whitelist_code_id: 234u64, + coin_registry_address: "coin_registry".to_string(), + }; + + let factory_instance = app + .instantiate_contract( + factory_code_id, + owner.clone(), + &init_msg, + &[], + "FACTORY", + None, + ) + .unwrap(); + + let msg = FactoryExecuteMsg::CreatePair { + pair_type: PairType::Stable {}, + asset_infos: vec![ + AssetInfo::Token { + contract_addr: token_x_instance.clone(), + }, + AssetInfo::Token { + contract_addr: token_y_instance.clone(), + }, + ], + init_params: Some( + to_json_binary(&StablePoolParams { + amp: 100, + owner: Some(owner.to_string()), + }) + .unwrap(), + ), + }; + + app.execute_contract(owner.clone(), factory_instance.clone(), &msg, &[]) + .unwrap(); + + let msg = FactoryQueryMsg::Pair { + asset_infos: vec![ + AssetInfo::Token { + contract_addr: token_x_instance.clone(), + }, + AssetInfo::Token { + contract_addr: token_y_instance.clone(), + }, + ], + }; + + let res: PairInfo = app + .wrap() + .query_wasm_smart(&factory_instance, &msg) + .unwrap(); + + let pair_instance = res.contract_addr; + + let msg = Cw20ExecuteMsg::IncreaseAllowance { + spender: pair_instance.to_string(), + expires: None, + amount: x_amount + x_offer, + }; + + app.execute_contract(owner.clone(), token_x_instance.clone(), &msg, &[]) + .unwrap(); + + let msg = Cw20ExecuteMsg::IncreaseAllowance { + spender: pair_instance.to_string(), + expires: None, + amount: y_amount, + }; + + app.execute_contract(owner.clone(), token_y_instance.clone(), &msg, &[]) + .unwrap(); + + let msg = ExecuteMsg::ProvideLiquidity { + assets: vec![ + Asset { + info: AssetInfo::Token { + contract_addr: token_x_instance.clone(), + }, + amount: x_amount, + }, + Asset { + info: AssetInfo::Token { + contract_addr: token_y_instance.clone(), + }, + amount: y_amount, + }, + ], + slippage_tolerance: None, + receiver: None, + }; + + app.execute_contract(owner.clone(), pair_instance.clone(), &msg, &[]) + .unwrap(); + + let d: u128 = app + .wrap() + .query_wasm_smart(&pair_instance, &QueryMsg::QueryComputeD {}) + .unwrap(); + assert_eq!(d, 2000000000000); + + // Set up 10% fee sharing + let fee_share_address = "contract_receiver".to_string(); + + let msg = ExecuteMsg::UpdateConfig { + params: to_json_binary(&StablePoolUpdateParams::EnableFeeShare { + fee_share_bps, + fee_share_address: fee_share_address.clone(), + }) + .unwrap(), + }; + + app.execute_contract(owner.clone(), pair_instance.clone(), &msg, &[]) + .unwrap(); + + let user = Addr::unchecked("user"); + + let msg = Cw20ExecuteMsg::Send { + contract: pair_instance.to_string(), + msg: to_json_binary(&Cw20HookMsg::Swap { + ask_asset_info: None, + belief_price: None, + max_spread: None, + to: Some(user.to_string()), + }) + .unwrap(), + amount: x_offer, + }; + + app.execute_contract(owner.clone(), token_x_instance.clone(), &msg, &[]) + .unwrap(); + + let y_expected_return = + x_offer - Uint128::from((x_offer * Decimal::from_ratio(total_fee_bps, 10000u64)).u128()); + + let msg = Cw20QueryMsg::Balance { + address: user.to_string(), + }; + + let res: BalanceResponse = app + .wrap() + .query_wasm_smart(&token_y_instance, &msg) + .unwrap(); + + assert_eq!(res.balance, y_expected_return); + + let msg = Cw20QueryMsg::Balance { + address: fee_share_address.to_string(), + }; + + let res: BalanceResponse = app + .wrap() + .query_wasm_smart(&token_y_instance, &msg) + .unwrap(); + + assert_eq!(res.balance, expected_fee_share); + + let msg = Cw20QueryMsg::Balance { + address: maker_address.to_string(), + }; + + let res: BalanceResponse = app + .wrap() + .query_wasm_smart(&token_y_instance, &msg) + .unwrap(); + + assert_eq!(res.balance, expected_maker_fee); + + app.update_block(|b| b.height += 1); + + // Assert LP balances are correct + let msg = QueryMsg::Pool {}; + let res: PoolResponse = app.wrap().query_wasm_smart(&pair_instance, &msg).unwrap(); + + assert_eq!(res.assets[0].amount, x_amount + x_offer); + assert_eq!( + res.assets[1].amount, + y_amount - y_expected_return - expected_maker_fee - expected_fee_share + ); +} diff --git a/contracts/pair_stable/tests/stablepool_tests.rs b/contracts/pair_stable/tests/stablepool_tests.rs new file mode 100644 index 0000000..0bd0858 --- /dev/null +++ b/contracts/pair_stable/tests/stablepool_tests.rs @@ -0,0 +1,591 @@ +#![cfg(not(tarpaulin_include))] + +use cosmwasm_std::{Addr, Decimal, StdError}; +use itertools::Itertools; +use std::str::FromStr; + +use helper::AppExtension; +use palomadex::asset::AssetInfoExt; +use palomadex::cosmwasm_ext::AbsDiff; +use palomadex::observation::OracleObservation; +use palomadex_pair_stable::error::ContractError; + +use crate::helper::{f64_to_dec, Helper, TestCoin}; + +mod helper; + +#[ignore] +#[test] +fn provide_and_withdraw_no_fee() { + let owner = Addr::unchecked("owner"); + + let test_coins = vec![ + TestCoin::native("uluna"), + TestCoin::cw20("USDC"), + TestCoin::cw20("USDD"), + ]; + + let mut helper = Helper::new(&owner, test_coins.clone(), 100u64, Some(0u16)).unwrap(); + + let user1 = Addr::unchecked("user1"); + let assets = vec![ + helper.assets[&test_coins[0]].with_balance(100_000000u128), + helper.assets[&test_coins[1]].with_balance(100_000000u128), + helper.assets[&test_coins[2]].with_balance(100_000000u128), + ]; + helper.give_me_money(&assets, &user1); + + helper.provide_liquidity(&user1, &assets).unwrap(); + + assert_eq!(299999000, helper.token_balance(&helper.lp_token, &user1)); + assert_eq!(0, helper.coin_balance(&test_coins[0], &user1)); + assert_eq!(0, helper.coin_balance(&test_coins[1], &user1)); + assert_eq!(0, helper.coin_balance(&test_coins[2], &user1)); + + // The user2 with the same assets should receive the same share + let user2 = Addr::unchecked("user2"); + let assets = vec![ + helper.assets[&test_coins[0]].with_balance(100_000000u128), + helper.assets[&test_coins[1]].with_balance(100_000000u128), + helper.assets[&test_coins[2]].with_balance(100_000000u128), + ]; + helper.give_me_money(&assets, &user2); + helper.provide_liquidity(&user2, &assets).unwrap(); + assert_eq!(300_000000, helper.token_balance(&helper.lp_token, &user2)); + + // The user3 makes imbalanced provide thus he is charged with fees + let user3 = Addr::unchecked("user3"); + let assets = vec![ + helper.assets[&test_coins[0]].with_balance(200_000000u128), + helper.assets[&test_coins[1]].with_balance(100_000000u128), + ]; + helper.give_me_money(&assets, &user3); + helper.provide_liquidity(&user3, &assets).unwrap(); + assert_eq!(299_629321, helper.token_balance(&helper.lp_token, &user3)); + + // Providing last asset with explicit zero amount should give nearly the same result + let user4 = Addr::unchecked("user4"); + let assets = vec![ + helper.assets[&test_coins[0]].with_balance(200_000000u128), + helper.assets[&test_coins[1]].with_balance(100_000000u128), + helper.assets[&test_coins[2]].with_balance(0u128), + ]; + helper.give_me_money(&assets, &user4); + helper.provide_liquidity(&user4, &assets).unwrap(); + assert_eq!(299_056292, helper.token_balance(&helper.lp_token, &user4)); + + helper + .withdraw_liquidity(&user1, 299999000, vec![]) + .unwrap(); + + assert_eq!(0, helper.token_balance(&helper.lp_token, &user1)); + // Previous imbalanced provides resulted in different share in assets + assert_eq!(150163977, helper.coin_balance(&test_coins[0], &user1)); + assert_eq!(100109318, helper.coin_balance(&test_coins[1], &user1)); + assert_eq!(50054659, helper.coin_balance(&test_coins[2], &user1)); + + // Checking imbalanced withdraw. Withdrawing only the first asset x 300 with the whole amount of LP tokens + helper + .withdraw_liquidity( + &user2, + 300_000000, + vec![helper.assets[&test_coins[0]].with_balance(300_000000u128)], + ) + .unwrap(); + + // Previous imbalanced provides resulted in small LP balance residual + assert_eq!(619390, helper.token_balance(&helper.lp_token, &user2)); + assert_eq!(300_000000, helper.coin_balance(&test_coins[0], &user2)); + assert_eq!(0, helper.coin_balance(&test_coins[1], &user2)); + assert_eq!(0, helper.coin_balance(&test_coins[2], &user2)); + + // Trying to receive more than possible + let err = helper + .withdraw_liquidity( + &user3, + 100_000000, + vec![helper.assets[&test_coins[1]].with_balance(101_000000u128)], + ) + .unwrap_err(); + assert_eq!( + "Generic error: Not enough LP tokens. You need 100679731 LP tokens.", + err.root_cause().to_string() + ); + + // Providing more LP tokens than needed. The rest will be kept on the user's balance + helper + .withdraw_liquidity( + &user3, + 200_892384, + vec![helper.assets[&test_coins[1]].with_balance(101_000000u128)], + ) + .unwrap(); + + // initial balance - spent amount; 100 goes back to the user3 + assert_eq!( + 299_629321 - 100679731, + helper.token_balance(&helper.lp_token, &user3) + ); + assert_eq!(0, helper.coin_balance(&test_coins[0], &user3)); + assert_eq!(101_000000, helper.coin_balance(&test_coins[1], &user3)); + assert_eq!(0, helper.coin_balance(&test_coins[2], &user3)); +} + +#[test] +fn provide_with_different_precision() { + let owner = Addr::unchecked("owner"); + + let test_coins = vec![ + TestCoin::cw20precise("FOO", 4), + TestCoin::cw20precise("BAR", 5), + ]; + + let mut helper = Helper::new(&owner, test_coins.clone(), 100u64, None).unwrap(); + + for user_name in ["user1", "user2"] { + let user = Addr::unchecked(user_name); + + let assets = vec![ + helper.assets[&test_coins[0]].with_balance(100_0000u128), + helper.assets[&test_coins[1]].with_balance(100_00000u128), + ]; + helper.give_me_money(&assets, &user); + + helper.provide_liquidity(&user, &assets).unwrap(); + } + + let user1 = Addr::unchecked("user1"); + + assert_eq!(19999000, helper.token_balance(&helper.lp_token, &user1)); + assert_eq!(0, helper.coin_balance(&test_coins[0], &user1)); + assert_eq!(0, helper.coin_balance(&test_coins[1], &user1)); + + helper.withdraw_liquidity(&user1, 19999000, vec![]).unwrap(); + + assert_eq!(0, helper.token_balance(&helper.lp_token, &user1)); + assert_eq!(999950, helper.coin_balance(&test_coins[0], &user1)); + assert_eq!(9999500, helper.coin_balance(&test_coins[1], &user1)); + + let user2 = Addr::unchecked("user2"); + assert_eq!(20000000, helper.token_balance(&helper.lp_token, &user2)); + assert_eq!(0, helper.coin_balance(&test_coins[0], &user2)); + assert_eq!(0, helper.coin_balance(&test_coins[1], &user2)); + + helper.withdraw_liquidity(&user2, 20000000, vec![]).unwrap(); + + assert_eq!(0, helper.token_balance(&helper.lp_token, &user2)); + assert_eq!(999999, helper.coin_balance(&test_coins[0], &user2)); + assert_eq!(9999999, helper.coin_balance(&test_coins[1], &user2)); +} + +#[test] +fn swap_different_precisions() { + let owner = Addr::unchecked("owner"); + + let test_coins = vec![ + TestCoin::cw20precise("FOO", 4), + TestCoin::cw20precise("BAR", 5), + ]; + + let mut helper = Helper::new(&owner, test_coins.clone(), 100u64, None).unwrap(); + + let assets = vec![ + helper.assets[&test_coins[0]].with_balance(100_000_0000u128), + helper.assets[&test_coins[1]].with_balance(100_000_00000u128), + ]; + helper.provide_liquidity(&owner, &assets).unwrap(); + + let user = Addr::unchecked("user"); + // 100 x FOO tokens + let offer_asset = helper.assets[&test_coins[0]].with_balance(100_0000u128); + // Checking direct swap simulation + let sim_resp = helper + .simulate_swap(&offer_asset, Some(helper.assets[&test_coins[1]].clone())) + .unwrap(); + // And reverse swap as well + let reverse_sim_resp = helper + .simulate_reverse_swap( + &helper.assets[&test_coins[1]].with_balance(sim_resp.return_amount.u128()), + Some(helper.assets[&test_coins[0]].clone()), + ) + .unwrap(); + assert_eq!(offer_asset.amount, reverse_sim_resp.offer_amount); + + helper.give_me_money(&[offer_asset.clone()], &user); + helper + .swap( + &user, + &offer_asset, + Some(helper.assets[&test_coins[1]].clone()), + ) + .unwrap(); + assert_eq!(0, helper.coin_balance(&test_coins[0], &user)); + // 99.94902 x BAR tokens + assert_eq!(99_94902, sim_resp.return_amount.u128()); + assert_eq!(99_94902, helper.coin_balance(&test_coins[1], &user)); +} + +#[ignore] +#[test] +fn check_swaps() { + let owner = Addr::unchecked("owner"); + + let test_coins = vec![ + TestCoin::native("uluna"), + TestCoin::cw20("USDC"), + TestCoin::cw20("USDD"), + ]; + + let mut helper = Helper::new(&owner, test_coins.clone(), 100u64, None).unwrap(); + + let assets = vec![ + helper.assets[&test_coins[0]].with_balance(100_000_000000u128), + helper.assets[&test_coins[1]].with_balance(100_000_000000u128), + helper.assets[&test_coins[2]].with_balance(100_000_000000u128), + ]; + helper.provide_liquidity(&owner, &assets).unwrap(); + + let user = Addr::unchecked("user"); + let offer_asset = helper.assets[&test_coins[0]].with_balance(100_000000u128); + helper.give_me_money(&[offer_asset.clone()], &user); + + let err = helper.swap(&user, &offer_asset, None).unwrap_err(); + assert_eq!( + ContractError::VariableAssetMissed {}, + err.downcast().unwrap() + ); + + let err = helper + .swap( + &user, + &offer_asset, + Some(helper.assets[&test_coins[0]].clone()), + ) + .unwrap_err(); + assert_eq!(ContractError::SameAssets {}, err.downcast().unwrap()); + + helper + .swap( + &user, + &offer_asset, + Some(helper.assets[&test_coins[1]].clone()), + ) + .unwrap(); + assert_eq!(0, helper.coin_balance(&test_coins[0], &user)); + assert_eq!(99_949011, helper.coin_balance(&test_coins[1], &user)); +} + +#[test] +fn check_wrong_initializations() { + let owner = Addr::unchecked("owner"); + + let err = Helper::new(&owner, vec![TestCoin::native("uluna")], 100u64, None).unwrap_err(); + + assert_eq!( + ContractError::InvalidNumberOfAssets(2), + err.downcast().unwrap() + ); + + let err = Helper::new( + &owner, + vec![ + TestCoin::native("one"), + TestCoin::cw20("two"), + TestCoin::native("three"), + TestCoin::cw20("four"), + TestCoin::native("five"), + TestCoin::cw20("six"), + ], + 100u64, + None, + ) + .unwrap_err(); + + assert_eq!( + ContractError::InvalidNumberOfAssets(2), + err.downcast().unwrap() + ); + + let err = Helper::new( + &owner, + vec![TestCoin::native("uluna"), TestCoin::native("uluna")], + 100u64, + None, + ) + .unwrap_err(); + + assert_eq!( + err.root_cause().to_string(), + "Doubling assets in asset infos" + ); + + // 2 assets in the pool is okay + Helper::new( + &owner, + vec![TestCoin::native("one"), TestCoin::cw20("two")], + 100u64, + None, + ) + .unwrap(); +} + +#[ignore] +#[test] +fn check_withdraw_charges_fees() { + let owner = Addr::unchecked("owner"); + + let test_coins = vec![ + TestCoin::native("uluna"), + TestCoin::cw20("USDC"), + TestCoin::cw20("USDD"), + ]; + + let mut helper = Helper::new(&owner, test_coins.clone(), 100u64, None).unwrap(); + + let assets = vec![ + helper.assets[&test_coins[0]].with_balance(100_000_000_000000u128), + helper.assets[&test_coins[1]].with_balance(100_000_000_000000u128), + helper.assets[&test_coins[2]].with_balance(100_000_000_000000u128), + ]; + helper.provide_liquidity(&owner, &assets).unwrap(); + + let user1 = Addr::unchecked("user1"); + let offer_asset = helper.assets[&test_coins[0]].with_balance(100_000000u128); + + // Usual swap for reference + helper.give_me_money(&[offer_asset.clone()], &user1); + helper + .swap( + &user1, + &offer_asset, + Some(helper.assets[&test_coins[1]].clone()), + ) + .unwrap(); + let usual_swap_amount = helper.coin_balance(&test_coins[1], &user1); + assert_eq!(99_950000, usual_swap_amount); + + // Trying to swap LUNA -> USDC via provide/withdraw + let user2 = Addr::unchecked("user2"); + helper.give_me_money(&[offer_asset.clone()], &user2); + + // Provide 100 x LUNA + helper.provide_liquidity(&user2, &[offer_asset]).unwrap(); + + // Withdraw 100 x USDC + let lp_tokens_amount = helper.token_balance(&helper.lp_token, &user2); + let err = helper + .withdraw_liquidity( + &user2, + lp_tokens_amount, + vec![helper.assets[&test_coins[1]].with_balance(100_000000u128)], + ) + .unwrap_err(); + assert_eq!( + err.root_cause().to_string(), + "Generic error: Not enough LP tokens. You need 100025002 LP tokens." + ); + + helper + .withdraw_liquidity( + &user2, + lp_tokens_amount, + vec![helper.assets[&test_coins[1]].with_balance(usual_swap_amount)], + ) + .unwrap(); + + // A small residual of LP tokens is left + assert_eq!(8, helper.token_balance(&helper.lp_token, &user2)); + assert_eq!( + usual_swap_amount, + helper.coin_balance(&test_coins[1], &user2) + ); +} + +#[test] +fn check_twap_based_prices() { + let owner = Addr::unchecked("owner"); + + let test_coins = vec![TestCoin::native("uusd"), TestCoin::cw20("USDX")]; + + let mut helper = Helper::new(&owner, test_coins.clone(), 100u64, None).unwrap(); + + let check_prices = |helper: &Helper| { + let prices = helper.query_prices().unwrap(); + + test_coins + .iter() + .cartesian_product(test_coins.iter()) + .filter(|(a, b)| a != b) + .for_each(|(from_coin, to_coin)| { + let price = prices + .cumulative_prices + .iter() + .filter(|(from, to, _)| { + from.eq(&helper.assets[&from_coin]) && to.eq(&helper.assets[&to_coin]) + }) + .collect::>(); + assert_eq!(price.len(), 1); + assert!(!price[0].2.is_zero()); + }); + }; + + let assets = vec![ + helper.assets[&test_coins[0]].with_balance(100_000_000_000000u128), + helper.assets[&test_coins[1]].with_balance(100_000_000_000000u128), + ]; + helper.provide_liquidity(&owner, &assets).unwrap(); + helper.app.next_block(1000); + check_prices(&helper); + + let user1 = Addr::unchecked("user1"); + let offer_asset = helper.assets[&test_coins[0]].with_balance(1000_000000u128); + helper.give_me_money(&[offer_asset.clone()], &user1); + + helper + .swap( + &user1, + &offer_asset, + Some(helper.assets[&test_coins[1]].clone()), + ) + .unwrap(); + + helper.app.next_block(86400); + check_prices(&helper); + + let assets = vec![ + helper.assets[&test_coins[0]].with_balance(100_000000u128), + helper.assets[&test_coins[1]].with_balance(100_000000u128), + ]; + helper.give_me_money(&assets, &user1); + + // Imbalanced provide + helper.provide_liquidity(&user1, &assets).unwrap(); + helper.app.next_block(14 * 86400); + check_prices(&helper); + + let offer_asset = helper.assets[&test_coins[1]].with_balance(10_000_000000u128); + helper.give_me_money(&[offer_asset.clone()], &user1); + helper + .swap( + &user1, + &offer_asset, + Some(helper.assets[&test_coins[0]].clone()), + ) + .unwrap(); + helper.app.next_block(86400); + check_prices(&helper); +} + +#[test] +fn check_pool_prices() { + let owner = Addr::unchecked("owner"); + + let test_coins = vec![TestCoin::native("uusd"), TestCoin::cw20("USDX")]; + + let mut helper = Helper::new(&owner, test_coins.clone(), 100u64, None).unwrap(); + + let assets = vec![ + helper.assets[&test_coins[0]].with_balance(100_000_000_000000u128), + helper.assets[&test_coins[1]].with_balance(100_000_000_000000u128), + ]; + helper.provide_liquidity(&owner, &assets).unwrap(); + helper.app.next_block(1000); + + let err = helper.query_observe(0).unwrap_err(); + assert_eq!( + err, + StdError::generic_err("Querier contract error: Generic error: Buffer is empty") + ); + + let user1 = Addr::unchecked("user1"); + let offer_asset = helper.assets[&test_coins[0]].with_balance(1000_000000u128); + helper.give_me_money(&[offer_asset.clone()], &user1); + + helper + .swap( + &user1, + &offer_asset, + Some(helper.assets[&test_coins[1]].clone()), + ) + .unwrap(); + + helper.app.next_block(86400); + assert_eq!( + helper.query_observe(0).unwrap(), + OracleObservation { + timestamp: helper.app.block_info().time.seconds(), + price: Decimal::from_str("1.000500348223145698").unwrap() + } + ); + + let assets = vec![ + helper.assets[&test_coins[0]].with_balance(100_000000u128), + helper.assets[&test_coins[1]].with_balance(100_000000u128), + ]; + helper.give_me_money(&assets, &user1); + + // Imbalanced provide + helper.provide_liquidity(&user1, &assets).unwrap(); + helper.app.next_block(14 * 86400); + + let offer_asset = helper.assets[&test_coins[1]].with_balance(10_000_000000u128); + helper.give_me_money(&[offer_asset.clone()], &user1); + helper + .swap( + &user1, + &offer_asset, + Some(helper.assets[&test_coins[0]].clone()), + ) + .unwrap(); + + // One more swap to trigger price update in the next step + helper + .swap( + &owner, + &offer_asset, + Some(helper.assets[&test_coins[0]].clone()), + ) + .unwrap(); + + helper.app.next_block(86400); + + assert_eq!( + helper.query_observe(0).unwrap(), + OracleObservation { + timestamp: helper.app.block_info().time.seconds(), + price: Decimal::from_str("0.999999778261572849").unwrap() + } + ); + + let price1 = helper.observe_price(0).unwrap(); + helper.app.next_block(10); + // Swapping the lowest amount possible which results in positive return amount + helper + .swap( + &user1, + &helper.assets[&test_coins[1]].with_balance(2u128), + None, + ) + .unwrap(); + let price2 = helper.observe_price(0).unwrap(); + // With such a small swap size contract doesn't store observation + assert_eq!(price1, price2); + + helper.app.next_block(10); + // Swap the smallest possible amount which gets observation saved + helper + .swap( + &user1, + &helper.assets[&test_coins[1]].with_balance(1005u128), + None, + ) + .unwrap(); + let price3 = helper.observe_price(0).unwrap(); + // Prove that price didn't jump that much + let diff = price3.diff(price2); + assert!( + diff / price2 < f64_to_dec(0.005), + "price jumped from {price2} to {price3} which is more than 0.5%" + ); + helper.app.next_block(10); +} diff --git a/contracts/router/.cargo/config.toml b/contracts/router/.cargo/config.toml new file mode 100644 index 0000000..eecbcfe --- /dev/null +++ b/contracts/router/.cargo/config.toml @@ -0,0 +1,6 @@ +[alias] +wasm = "build --release --target wasm32-unknown-unknown" +wasm-debug = "build --target wasm32-unknown-unknown" +unit-test = "test --lib" +integration-test = "test --test integration" +schema = "run --example router_schema" diff --git a/contracts/router/.editorconfig b/contracts/router/.editorconfig new file mode 100644 index 0000000..3d36f20 --- /dev/null +++ b/contracts/router/.editorconfig @@ -0,0 +1,11 @@ +root = true + +[*] +indent_style = space +indent_size = 2 +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.rs] +indent_size = 4 diff --git a/contracts/router/.gitignore b/contracts/router/.gitignore new file mode 100644 index 0000000..10fe5d6 --- /dev/null +++ b/contracts/router/.gitignore @@ -0,0 +1,12 @@ +# Build results +/target + +# Text file backups +**/*.rs.bk + +# macOS +.DS_Store + +# IDEs +*.iml +.idea diff --git a/contracts/router/Cargo.toml b/contracts/router/Cargo.toml new file mode 100644 index 0000000..43630c8 --- /dev/null +++ b/contracts/router/Cargo.toml @@ -0,0 +1,41 @@ +[package] +name = "palomadex-router" +version = "0.1.0" +authors = ["Volume Finance"] +edition = "2021" + +exclude = [ + # Those files are rust-optimizer artifacts. You might want to commit them for convenience but they should not be part of the source code publication. + "contract.wasm", + "hash.txt", +] + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +# for quicker tests, cargo test --lib +# for more explicit tests, cargo test --features=backtraces +library = [] +backtraces = ["cosmwasm-std/backtraces"] + +[lints.rust] +unexpected_cfgs = { level = "warn", check-cfg = ["cfg(tarpaulin_include)"] } + +[dependencies] +cw20.workspace = true +cosmwasm-std.workspace = true +cw-storage-plus.workspace = true +integer-sqrt = "0.1" +palomadex = { path = "../../packages/palomadex"} +thiserror.workspace = true +cosmwasm-schema.workspace = true + +[dev-dependencies] +palomadex-factory = { path = "../factory" } +cw20-base = "1.1" +palomadex-pair = { path = "../pair" } +anyhow = "1.0" +cw-multi-test = "1.2.0" diff --git a/contracts/router/README.md b/contracts/router/README.md new file mode 100644 index 0000000..2c35b94 --- /dev/null +++ b/contracts/router/README.md @@ -0,0 +1,211 @@ +# Palomadex Router + +The Router contract contains logic to facilitate multi-hop swaps for Terra native & Palomadex tokens. + +--- + +### Operations Assertion + +For every swap, the contract checks if the resulting token is the one that was asked for and whether the receiving amount exceeds the minimum to receive. + +## InstantiateMsg + +Initializes the contract with the Palomadex factory contract address. + +```json +{ + "palomadex_factory": "terra..." +} +``` + +## ExecuteMsg + +### `receive` + +CW20 receive msg. + +```json +{ + "receive": { + "sender": "terra...", + "amount": "123", + "msg": "" + } +} +``` + +### `execute_swap_operation` + +Swaps one token to another. _single_ defines whether this swap is single or part of a multi hop route. +This message is for internal use. + +### Example + +Swap UST => mABNB + +```json +{ + "execute_swap_operation": { + "operation": { + "astro_swap": { + "offer_asset_info": { + "native_token": { + "denom": "uusd" + } + }, + "ask_asset_info": { + "token": { + "contract_addr": "terra..." + } + } + } + }, + "to": "terra...", + "max_spread": "0.05", + "single": false + } +} +``` + +### `execute_swap_operations` + +Performs multi-hop swap operations for native & Palomadex tokens. Swaps execute one-by-one and the last swap will return the ask token. This function is public (can be called by anyone). +Contract sets total 'return_amount' in response data after all routes are processed. See `SwapResponseData` type for more info. +Note: Response data makes sense ONLY if the first token in multi-hop swap is native. Otherwise, cw20::send message resets response data. + +### Example + +Swap KRT => UST => mABNB + +```json +{ + "execute_swap_operations": { + "operations": [ + { + "native_swap":{ + "offer_denom":"ukrw", + "ask_denom":"uusd" + } + }, + { + "astro_swap": { + "offer_asset_info": { + "native_token": { + "denom": "uusd" + } + }, + "ask_asset_info": { + "token": { + "contract_addr": "terra..." + } + } + } + } + ], + "minimum_receive": "123", + "to": "terra...", + "max_spread": "0.05" + } +} +``` + +### `assert_minimum_receive` + +Checks that an amount of ask tokens exceeds `minimum_receive`. This message is for internal use. + +```json +{ + "assert_minimum_receive": { + "asset_info": { + "token": { + "contract_addr": "terra..." + } + }, + "prev_balance": "123", + "minimum_receive": "123", + "receiver": "terra..." + } +} +``` + +## QueryMsg + +All query messages are described below. A custom struct is defined for each query response. + +### `config` + +Returns the general configuration for the router contract. + +```json +{ + "config": {} +} +``` + +### `simulate_swap_operations` + +Simulates multi-hop swap operations. Examples: + +- KRT => UST => mABNB + +```json +{ + "simulate_swap_operations" : { + "offer_amount": "123", + "operations": [ + { + "native_swap": { + "offer_denom": "ukrw", + "ask_denom": "uusd" + } + }, + { + "astro_swap": { + "offer_asset_info": { + "native_token": { + "denom": "uusd" + } + }, + "ask_asset_info": { + "token": { + "contract_addr": "terra..." + } + } + } + } + ] + } +} +``` + +- mABNB => UST => KRT + +```json +{ + "simulate_swap_operations" : { + "offer_amount": "123", + "operations": [ + { + "native_swap": { + "offer_denom": "uusd", + "ask_denom": "ukrw" + } + }, + { + "astro_swap": { + "offer_asset_info": { + "token": { + "contract_addr": "terra..." + } + }, + "ask_asset_info": { + "native_token": { + "denom": "uusd" + } + } + } + } + ] + } +} +``` diff --git a/contracts/router/examples/router_schema.rs b/contracts/router/examples/router_schema.rs new file mode 100644 index 0000000..b800e56 --- /dev/null +++ b/contracts/router/examples/router_schema.rs @@ -0,0 +1,10 @@ +use cosmwasm_schema::write_api; +use palomadex::router::{ExecuteMsg, InstantiateMsg, QueryMsg}; + +fn main() { + write_api! { + instantiate: InstantiateMsg, + query: QueryMsg, + execute: ExecuteMsg, + } +} diff --git a/contracts/router/src/contract.rs b/contracts/router/src/contract.rs new file mode 100644 index 0000000..5051984 --- /dev/null +++ b/contracts/router/src/contract.rs @@ -0,0 +1,467 @@ +use cosmwasm_std::{ + entry_point, from_json, to_json_binary, wasm_execute, Addr, Api, Binary, Decimal, Deps, + DepsMut, Env, MessageInfo, Reply, Response, StdError, StdResult, SubMsg, SubMsgResult, Uint128, +}; +use cw20::Cw20ReceiveMsg; + +use palomadex::asset::{addr_opt_validate, Asset, AssetInfo}; +use palomadex::pair::{QueryMsg as PairQueryMsg, SimulationResponse}; +use palomadex::querier::query_pair_info; +use palomadex::router::{ + ConfigResponse, Cw20HookMsg, ExecuteMsg, InstantiateMsg, QueryMsg, + SimulateSwapOperationsResponse, SwapOperation, SwapResponseData, MAX_SWAP_OPERATIONS, +}; + +use crate::error::ContractError; +use crate::operations::execute_swap_operation; +use crate::state::{Config, ReplyData, CONFIG, REPLY_DATA}; + +pub const AFTER_SWAP_REPLY_ID: u64 = 1; + +/// Creates a new contract with the specified parameters in the [`InstantiateMsg`]. +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn instantiate( + deps: DepsMut, + _env: Env, + _info: MessageInfo, + msg: InstantiateMsg, +) -> Result { + CONFIG.save( + deps.storage, + &Config { + palomadex_factory: deps.api.addr_validate(&msg.palomadex_factory)?, + }, + )?; + + Ok(Response::default()) +} + +/// Exposes all the execute functions available in the contract. +/// +/// ## Variants +/// * **ExecuteMsg::Receive(msg)** Receives a message of type [`Cw20ReceiveMsg`] and processes +/// it depending on the received template. +/// +/// * **ExecuteMsg::ExecuteSwapOperations { +/// operations, +/// minimum_receive, +/// to +/// }** Performs swap operations with the specified parameters. +/// +/// * **ExecuteMsg::ExecuteSwapOperation { operation, to }** Execute a single swap operation. +/// +/// * **ExecuteMsg::AssertMinimumReceive { +/// asset_info, +/// prev_balance, +/// minimum_receive, +/// receiver +/// }** Checks if an ask amount is higher than or equal to the minimum amount to receive. +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn execute( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: ExecuteMsg, +) -> Result { + match msg { + ExecuteMsg::Receive(msg) => receive_cw20(deps, env, msg), + ExecuteMsg::ExecuteSwapOperations { + operations, + minimum_receive, + to, + max_spread, + } => execute_swap_operations( + deps, + env, + info.sender, + operations, + minimum_receive, + to, + max_spread, + ), + ExecuteMsg::ExecuteSwapOperation { + operation, + to, + max_spread, + single, + } => execute_swap_operation(deps, env, info, operation, to, max_spread, single), + } +} + +/// Receives a message of type [`Cw20ReceiveMsg`] and processes it depending on the received template. +/// +/// * **cw20_msg** is an object of type [`Cw20ReceiveMsg`]. +pub fn receive_cw20( + deps: DepsMut, + env: Env, + cw20_msg: Cw20ReceiveMsg, +) -> Result { + match from_json(&cw20_msg.msg)? { + Cw20HookMsg::ExecuteSwapOperations { + operations, + minimum_receive, + to, + max_spread, + } => execute_swap_operations( + deps, + env, + Addr::unchecked(cw20_msg.sender), + operations, + minimum_receive, + to, + max_spread, + ), + } +} + +/// Performs swap operations with the specified parameters. +/// +/// * **sender** address that swaps tokens. +/// +/// * **operations** all swap operations to perform. +/// +/// * **minimum_receive** used to guarantee that the ask amount is above a minimum amount. +/// +/// * **to** recipient of the ask tokens. +#[allow(clippy::too_many_arguments)] +pub fn execute_swap_operations( + deps: DepsMut, + env: Env, + sender: Addr, + operations: Vec, + minimum_receive: Option, + to: Option, + max_spread: Option, +) -> Result { + assert_operations(deps.api, &operations)?; + + let to = addr_opt_validate(deps.api, &to)?.unwrap_or(sender); + let target_asset_info = operations.last().unwrap().get_target_asset_info(); + let operations_len = operations.len(); + + let messages = operations + .into_iter() + .enumerate() + .map(|(operation_index, op)| { + if operation_index == operations_len - 1 { + wasm_execute( + env.contract.address.to_string(), + &ExecuteMsg::ExecuteSwapOperation { + operation: op, + to: Some(to.to_string()), + max_spread, + single: operations_len == 1, + }, + vec![], + ) + .map(|inner_msg| SubMsg::reply_on_success(inner_msg, AFTER_SWAP_REPLY_ID)) + } else { + wasm_execute( + env.contract.address.to_string(), + &ExecuteMsg::ExecuteSwapOperation { + operation: op, + to: None, + max_spread, + single: operations_len == 1, + }, + vec![], + ) + .map(SubMsg::new) + } + }) + .collect::>>()?; + + let prev_balance = target_asset_info.query_pool(&deps.querier, &to)?; + REPLY_DATA.save( + deps.storage, + &ReplyData { + asset_info: target_asset_info, + prev_balance, + minimum_receive, + receiver: to.to_string(), + }, + )?; + + Ok(Response::new().add_submessages(messages)) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn reply(deps: DepsMut, _env: Env, msg: Reply) -> Result { + match msg { + Reply { + id: AFTER_SWAP_REPLY_ID, + result: SubMsgResult::Ok(..), + } => { + let reply_data = REPLY_DATA.load(deps.storage)?; + let receiver_balance = reply_data + .asset_info + .query_pool(&deps.querier, reply_data.receiver)?; + let swap_amount = receiver_balance.checked_sub(reply_data.prev_balance)?; + + if let Some(minimum_receive) = reply_data.minimum_receive { + if swap_amount < minimum_receive { + return Err(ContractError::AssertionMinimumReceive { + receive: minimum_receive, + amount: swap_amount, + }); + } + } + + // Reply data makes sense ONLY if the first token in multi-hop swap is native. + let data = to_json_binary(&SwapResponseData { + return_amount: swap_amount, + })?; + + Ok(Response::new().set_data(data)) + } + _ => Err(StdError::generic_err("Failed to process reply").into()), + } +} + +/// Exposes all the queries available in the contract. +/// ## Queries +/// * **QueryMsg::Config {}** Returns general router parameters using a [`ConfigResponse`] object. +/// * **QueryMsg::SimulateSwapOperations { +/// offer_amount, +/// operations, +/// }** Simulates one or multiple swap operations and returns the end result in a [`SimulateSwapOperationsResponse`] object. +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> Result { + match msg { + QueryMsg::Config {} => Ok(to_json_binary(&query_config(deps)?)?), + QueryMsg::SimulateSwapOperations { + offer_amount, + operations, + } => Ok(to_json_binary(&simulate_swap_operations( + deps, + offer_amount, + operations, + )?)?), + } +} + +/// Returns general contract settings in a [`ConfigResponse`] object. +pub fn query_config(deps: Deps) -> Result { + let state = CONFIG.load(deps.storage)?; + let resp = ConfigResponse { + palomadex_factory: state.palomadex_factory.into_string(), + }; + + Ok(resp) +} + +/// Returns the end result of a simulation for one or multiple swap +/// operations using a [`SimulateSwapOperationsResponse`] object. +/// +/// * **offer_amount** amount of offer assets being swapped. +/// +/// * **operations** is a vector that contains objects of type [`SwapOperation`]. +/// These are all the swap operations for which we perform a simulation. +fn simulate_swap_operations( + deps: Deps, + offer_amount: Uint128, + operations: Vec, +) -> Result { + assert_operations(deps.api, &operations)?; + + let config = CONFIG.load(deps.storage)?; + let palomadex_factory = config.palomadex_factory; + let mut return_amount = offer_amount; + + for operation in operations.into_iter() { + match operation { + SwapOperation::AstroSwap { + offer_asset_info, + ask_asset_info, + } => { + let pair_info = query_pair_info( + &deps.querier, + palomadex_factory.clone(), + &[offer_asset_info.clone(), ask_asset_info.clone()], + )?; + + let res: SimulationResponse = deps.querier.query_wasm_smart( + pair_info.contract_addr, + &PairQueryMsg::Simulation { + offer_asset: Asset { + info: offer_asset_info.clone(), + amount: return_amount, + }, + ask_asset_info: Some(ask_asset_info.clone()), + }, + )?; + + return_amount = res.return_amount; + } + SwapOperation::NativeSwap { .. } => { + return Err(ContractError::NativeSwapNotSupported {}) + } + } + } + + Ok(SimulateSwapOperationsResponse { + amount: return_amount, + }) +} + +/// Validates swap operations. +/// +/// * **operations** is a vector that contains objects of type [`SwapOperation`]. These are all the swap operations we check. +fn assert_operations(api: &dyn Api, operations: &[SwapOperation]) -> Result<(), ContractError> { + let operations_len = operations.len(); + if operations_len == 0 { + return Err(ContractError::MustProvideOperations {}); + } + + if operations_len > MAX_SWAP_OPERATIONS { + return Err(ContractError::SwapLimitExceeded {}); + } + + let mut prev_ask_asset: Option = None; + + for operation in operations { + let (offer_asset, ask_asset) = match operation { + SwapOperation::AstroSwap { + offer_asset_info, + ask_asset_info, + } => (offer_asset_info.clone(), ask_asset_info.clone()), + SwapOperation::NativeSwap { .. } => { + return Err(ContractError::NativeSwapNotSupported {}) + } + }; + + offer_asset.check(api)?; + ask_asset.check(api)?; + + if offer_asset.equal(&ask_asset) { + return Err(ContractError::DoublingAssetsPath { + offer_asset: offer_asset.to_string(), + ask_asset: ask_asset.to_string(), + }); + } + + if let Some(prev_ask_asset) = prev_ask_asset { + if prev_ask_asset != offer_asset { + return Err(ContractError::InvalidPathOperations { + prev_ask_asset: prev_ask_asset.to_string(), + next_offer_asset: offer_asset.to_string(), + next_ask_asset: ask_asset.to_string(), + }); + } + } + + prev_ask_asset = Some(ask_asset); + } + + Ok(()) +} + +#[cfg(test)] +mod testing { + use super::*; + + #[test] + fn test_invalid_operations() { + use cosmwasm_std::testing::mock_dependencies; + let deps = mock_dependencies(); + // Empty error + assert_eq!(true, assert_operations(deps.as_ref().api, &[]).is_err()); + + // uluna output + assert_eq!( + true, + assert_operations( + deps.as_ref().api, + &vec![ + SwapOperation::AstroSwap { + offer_asset_info: AssetInfo::NativeToken { + denom: "ukrw".to_string(), + }, + ask_asset_info: AssetInfo::Token { + contract_addr: Addr::unchecked("asset0001"), + }, + }, + SwapOperation::AstroSwap { + offer_asset_info: AssetInfo::Token { + contract_addr: Addr::unchecked("asset0001"), + }, + ask_asset_info: AssetInfo::NativeToken { + denom: "uluna".to_string(), + }, + }, + ] + ) + .is_ok() + ); + + // asset0002 output + assert_eq!( + true, + assert_operations( + deps.as_ref().api, + &vec![ + SwapOperation::AstroSwap { + offer_asset_info: AssetInfo::NativeToken { + denom: "ukrw".to_string(), + }, + ask_asset_info: AssetInfo::Token { + contract_addr: Addr::unchecked("asset0001"), + }, + }, + SwapOperation::AstroSwap { + offer_asset_info: AssetInfo::Token { + contract_addr: Addr::unchecked("asset0001"), + }, + ask_asset_info: AssetInfo::NativeToken { + denom: "uluna".to_string(), + }, + }, + SwapOperation::AstroSwap { + offer_asset_info: AssetInfo::NativeToken { + denom: "uluna".to_string(), + }, + ask_asset_info: AssetInfo::Token { + contract_addr: Addr::unchecked("asset0002"), + }, + }, + ] + ) + .is_ok() + ); + + // Multiple output token type errors + assert_eq!( + true, + assert_operations( + deps.as_ref().api, + &vec![ + SwapOperation::AstroSwap { + offer_asset_info: AssetInfo::NativeToken { + denom: "ukrw".to_string(), + }, + ask_asset_info: AssetInfo::Token { + contract_addr: Addr::unchecked("asset0001"), + }, + }, + SwapOperation::AstroSwap { + offer_asset_info: AssetInfo::Token { + contract_addr: Addr::unchecked("asset0001"), + }, + ask_asset_info: AssetInfo::NativeToken { + denom: "uaud".to_string(), + }, + }, + SwapOperation::AstroSwap { + offer_asset_info: AssetInfo::NativeToken { + denom: "uluna".to_string(), + }, + ask_asset_info: AssetInfo::Token { + contract_addr: Addr::unchecked("asset0002"), + }, + }, + ] + ) + .is_err() + ); + } +} diff --git a/contracts/router/src/error.rs b/contracts/router/src/error.rs new file mode 100644 index 0000000..ff611fa --- /dev/null +++ b/contracts/router/src/error.rs @@ -0,0 +1,46 @@ +use cosmwasm_std::{OverflowError, StdError, Uint128}; +use thiserror::Error; + +/// This enum describes oracle contract errors +#[derive(Error, Debug, PartialEq)] +pub enum ContractError { + #[error("{0}")] + Std(#[from] StdError), + + #[error("{0}")] + OverflowError(#[from] OverflowError), + + #[error("Unauthorized")] + Unauthorized {}, + + #[error( + "The next offer asset must be the same as the previous ask asset; \ + {prev_ask_asset} --> {next_offer_asset} --> {next_ask_asset}" + )] + InvalidPathOperations { + prev_ask_asset: String, + next_offer_asset: String, + next_ask_asset: String, + }, + + #[error("Doubling assets in one batch of path; {offer_asset} --> {ask_asset}")] + DoublingAssetsPath { + offer_asset: String, + ask_asset: String, + }, + + #[error("Must specify swap operations!")] + MustProvideOperations {}, + + #[error("Assertion failed; minimum receive amount: {receive}, swap amount: {amount}")] + AssertionMinimumReceive { receive: Uint128, amount: Uint128 }, + + #[error("The swap operation limit was exceeded!")] + SwapLimitExceeded {}, + + #[error("Native swap operations are not supported!")] + NativeSwapNotSupported {}, + + #[error("Contract can't be migrated!")] + MigrationError {}, +} diff --git a/contracts/router/src/lib.rs b/contracts/router/src/lib.rs new file mode 100644 index 0000000..72b2715 --- /dev/null +++ b/contracts/router/src/lib.rs @@ -0,0 +1,9 @@ +pub mod contract; +pub mod state; + +pub mod error; + +mod operations; + +#[cfg(test)] +mod testing; diff --git a/contracts/router/src/operations.rs b/contracts/router/src/operations.rs new file mode 100644 index 0000000..2d46e2d --- /dev/null +++ b/contracts/router/src/operations.rs @@ -0,0 +1,131 @@ +use cosmwasm_std::{ + to_json_binary, Coin, CosmosMsg, Decimal, DepsMut, Env, MessageInfo, Response, StdResult, + WasmMsg, +}; +use cw20::Cw20ExecuteMsg; +use palomadex::asset::{Asset, AssetInfo}; +use palomadex::pair::ExecuteMsg as PairExecuteMsg; +use palomadex::querier::{query_balance, query_pair_info, query_token_balance}; +use palomadex::router::SwapOperation; + +use crate::error::ContractError; +use crate::state::CONFIG; + +/// Execute a swap operation. +/// +/// * **operation** to perform (native or Astro swap with offer and ask asset information). +/// +/// * **to** address that receives the ask assets. +/// +/// * **single** defines whether this swap is single or part of a multi hop route. +pub fn execute_swap_operation( + deps: DepsMut, + env: Env, + info: MessageInfo, + operation: SwapOperation, + to: Option, + max_spread: Option, + single: bool, +) -> Result { + if env.contract.address != info.sender { + return Err(ContractError::Unauthorized {}); + } + + let message = match operation { + SwapOperation::AstroSwap { + offer_asset_info, + ask_asset_info, + } => { + let config = CONFIG.load(deps.storage)?; + let pair_info = query_pair_info( + &deps.querier, + config.palomadex_factory, + &[offer_asset_info.clone(), ask_asset_info.clone()], + )?; + + let amount = match &offer_asset_info { + AssetInfo::NativeToken { denom } => { + query_balance(&deps.querier, env.contract.address, denom)? + } + AssetInfo::Token { contract_addr } => { + query_token_balance(&deps.querier, contract_addr, env.contract.address)? + } + }; + let offer_asset = Asset { + info: offer_asset_info, + amount, + }; + + asset_into_swap_msg( + pair_info.contract_addr.to_string(), + offer_asset, + ask_asset_info, + max_spread, + to, + single, + )? + } + SwapOperation::NativeSwap { .. } => return Err(ContractError::NativeSwapNotSupported {}), + }; + + Ok(Response::new().add_message(message)) +} + +/// Creates a message of type [`CosmosMsg`] representing a swap operation. +/// +/// * **pair_contract** Palomadex pair contract for which the swap operation is performed. +/// +/// * **offer_asset** asset that is swapped. It also mentions the amount to swap. +/// +/// * **ask_asset_info** asset that is swapped to. +/// +/// * **max_spread** max spread enforced for the swap. +/// +/// * **to** address that receives the ask assets. +/// +/// * **single** defines whether this swap is single or part of a multi hop route. +pub fn asset_into_swap_msg( + pair_contract: String, + offer_asset: Asset, + ask_asset_info: AssetInfo, + max_spread: Option, + to: Option, + single: bool, +) -> StdResult { + // Disabling spread assertion if this swap is part of a multi hop route + let belief_price = if single { None } else { Some(Decimal::MAX) }; + + match &offer_asset.info { + AssetInfo::NativeToken { denom } => Ok(CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: pair_contract, + funds: vec![Coin { + denom: denom.to_string(), + amount: offer_asset.amount, + }], + msg: to_json_binary(&PairExecuteMsg::Swap { + offer_asset: Asset { + amount: offer_asset.amount, + ..offer_asset + }, + ask_asset_info: Some(ask_asset_info), + belief_price, + max_spread, + to, + })?, + })), + AssetInfo::Token { contract_addr } => Ok(CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: contract_addr.to_string(), + funds: vec![], + msg: to_json_binary(&Cw20ExecuteMsg::Send { + contract: pair_contract, + amount: offer_asset.amount, + msg: to_json_binary(&palomadex::pair::Cw20HookMsg::Swap { + ask_asset_info: Some(ask_asset_info), + belief_price, + max_spread, + to, + })?, + })?, + })), + } +} diff --git a/contracts/router/src/state.rs b/contracts/router/src/state.rs new file mode 100644 index 0000000..df24139 --- /dev/null +++ b/contracts/router/src/state.rs @@ -0,0 +1,24 @@ +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{Addr, Uint128}; +use cw_storage_plus::Item; +use palomadex::asset::AssetInfo; + +/// Stores the contract config at the given key +pub const CONFIG: Item = Item::new("config"); + +/// This structure holds the main parameters for the router +#[cw_serde] +pub struct Config { + /// The factory contract address + pub palomadex_factory: Addr, +} + +pub const REPLY_DATA: Item = Item::new("reply_data"); + +#[cw_serde] +pub struct ReplyData { + pub asset_info: AssetInfo, + pub prev_balance: Uint128, + pub minimum_receive: Option, + pub receiver: String, +} diff --git a/contracts/router/src/testing/mock_querier.rs b/contracts/router/src/testing/mock_querier.rs new file mode 100644 index 0000000..af3694b --- /dev/null +++ b/contracts/router/src/testing/mock_querier.rs @@ -0,0 +1,239 @@ +use cosmwasm_schema::cw_serde; +use cosmwasm_std::testing::{MockApi, MockQuerier, MockStorage, MOCK_CONTRACT_ADDR}; +use cosmwasm_std::{ + from_json, to_json_binary, Addr, Binary, Coin, ContractResult, Empty, OwnedDeps, Querier, + QuerierResult, QueryRequest, SystemError, SystemResult, Uint128, WasmQuery, +}; +use std::collections::HashMap; + +use cw20::{BalanceResponse, Cw20QueryMsg, TokenInfoResponse}; +use palomadex::asset::{Asset, AssetInfo, PairInfo}; +use palomadex::factory::PairType; +use palomadex::pair::SimulationResponse; + +#[cw_serde] +pub enum QueryMsg { + Pair { + asset_infos: Vec, + }, + Simulation { + offer_asset: Asset, + ask_asset_info: Option, + }, +} + +/// mock_dependencies is a drop-in replacement for cosmwasm_std::testing::mock_dependencies. +/// This uses the Palomadex CustomQuerier. +pub fn mock_dependencies( + contract_balance: &[Coin], +) -> OwnedDeps { + let custom_querier: WasmMockQuerier = + WasmMockQuerier::new(MockQuerier::new(&[(MOCK_CONTRACT_ADDR, contract_balance)])); + + OwnedDeps { + storage: MockStorage::default(), + api: MockApi::default(), + querier: custom_querier, + custom_query_type: Default::default(), + } +} + +pub struct WasmMockQuerier { + base: MockQuerier, + token_querier: TokenQuerier, + palomadex_factory_querier: PalomadexFactoryQuerier, +} + +#[derive(Clone, Default)] +pub struct TokenQuerier { + // This lets us iterate over all pairs that match the first string + balances: HashMap>, +} + +impl TokenQuerier { + pub fn new(balances: &[(&String, &[(&String, &Uint128)])]) -> Self { + TokenQuerier { + balances: balances_to_map(balances), + } + } +} + +pub(crate) fn balances_to_map( + balances: &[(&String, &[(&String, &Uint128)])], +) -> HashMap> { + let mut balances_map: HashMap> = HashMap::new(); + for (contract_addr, balances) in balances.iter() { + let mut contract_balances_map: HashMap = HashMap::new(); + for (addr, balance) in balances.iter() { + contract_balances_map.insert(addr.to_string(), **balance); + } + + balances_map.insert(contract_addr.to_string(), contract_balances_map); + } + balances_map +} + +#[derive(Clone, Default)] +pub struct PalomadexFactoryQuerier { + pairs: HashMap, +} + +impl PalomadexFactoryQuerier { + pub fn new(pairs: &[(&String, &String)]) -> Self { + PalomadexFactoryQuerier { + pairs: pairs_to_map(pairs), + } + } +} + +pub(crate) fn pairs_to_map(pairs: &[(&String, &String)]) -> HashMap { + let mut pairs_map: HashMap = HashMap::new(); + for (key, pair) in pairs.iter() { + pairs_map.insert(key.to_string(), pair.to_string()); + } + pairs_map +} + +impl Querier for WasmMockQuerier { + fn raw_query(&self, bin_request: &[u8]) -> QuerierResult { + // MockQuerier doesn't support Custom, so we ignore it completely here + let request: QueryRequest = match from_json(bin_request) { + Ok(v) => v, + Err(e) => { + return SystemResult::Err(SystemError::InvalidRequest { + error: format!("Parsing query request: {}", e), + request: bin_request.into(), + }) + } + }; + self.handle_query(&request) + } +} + +#[cw_serde] +pub enum MockQueryMsg { + Price {}, +} + +impl WasmMockQuerier { + pub fn handle_query(&self, request: &QueryRequest) -> QuerierResult { + match &request { + QueryRequest::Wasm(WasmQuery::Smart { contract_addr, msg }) => { + if contract_addr.to_string().starts_with("token") + || contract_addr.to_string().starts_with("asset") + { + self.handle_cw20(&contract_addr, &msg) + } else { + self.handle_default(&msg) + } + } + _ => self.base.handle_query(request), + } + } + + fn handle_default(&self, msg: &Binary) -> QuerierResult { + match from_json(&msg).unwrap() { + QueryMsg::Pair { asset_infos } => { + let key = asset_infos[0].to_string() + asset_infos[1].to_string().as_str(); + match self.palomadex_factory_querier.pairs.get(&key) { + Some(v) => SystemResult::Ok(ContractResult::from(to_json_binary(&PairInfo { + contract_addr: Addr::unchecked(v), + liquidity_token: Addr::unchecked("liquidity"), + asset_infos: vec![ + AssetInfo::NativeToken { + denom: "uusd".to_string(), + }, + AssetInfo::NativeToken { + denom: "uusd".to_string(), + }, + ], + pair_type: PairType::Xyk {}, + }))), + None => SystemResult::Err(SystemError::InvalidRequest { + error: "No pair info exists".to_string(), + request: msg.as_slice().into(), + }), + } + } + QueryMsg::Simulation { offer_asset, .. } => { + SystemResult::Ok(ContractResult::from(to_json_binary(&SimulationResponse { + return_amount: offer_asset.amount, + commission_amount: Uint128::zero(), + spread_amount: Uint128::zero(), + }))) + } + } + } + + fn handle_cw20(&self, contract_addr: &String, msg: &Binary) -> QuerierResult { + match from_json(&msg).unwrap() { + Cw20QueryMsg::TokenInfo {} => { + let balances: &HashMap = + match self.token_querier.balances.get(contract_addr) { + Some(balances) => balances, + None => { + return SystemResult::Err(SystemError::Unknown {}); + } + }; + + let mut total_supply = Uint128::zero(); + + for balance in balances { + total_supply += *balance.1; + } + + SystemResult::Ok(ContractResult::from(to_json_binary(&TokenInfoResponse { + name: "mAPPL".to_string(), + symbol: "mAPPL".to_string(), + decimals: 6, + total_supply: total_supply, + }))) + } + Cw20QueryMsg::Balance { address } => { + let balances: &HashMap = + match self.token_querier.balances.get(contract_addr) { + Some(balances) => balances, + None => { + return SystemResult::Err(SystemError::Unknown {}); + } + }; + + let balance = match balances.get(&address) { + Some(v) => v, + None => { + return SystemResult::Err(SystemError::Unknown {}); + } + }; + + SystemResult::Ok(ContractResult::from(to_json_binary(&BalanceResponse { + balance: *balance, + }))) + } + _ => panic!("DO NOT ENTER HERE"), + } + } +} + +impl WasmMockQuerier { + pub fn new(base: MockQuerier) -> Self { + WasmMockQuerier { + base, + token_querier: TokenQuerier::default(), + palomadex_factory_querier: PalomadexFactoryQuerier::default(), + } + } + + pub fn with_balance(&mut self, balances: &[(&String, &[Coin])]) { + for (addr, balance) in balances { + self.base.update_balance(addr.as_str(), balance.to_vec()); + } + } + + pub fn with_token_balances(&mut self, balances: &[(&String, &[(&String, &Uint128)])]) { + self.token_querier = TokenQuerier::new(balances); + } + + pub fn with_palomadex_pairs(&mut self, pairs: &[(&String, &String)]) { + self.palomadex_factory_querier = PalomadexFactoryQuerier::new(pairs); + } +} diff --git a/contracts/router/src/testing/mod.rs b/contracts/router/src/testing/mod.rs new file mode 100644 index 0000000..a1e507b --- /dev/null +++ b/contracts/router/src/testing/mod.rs @@ -0,0 +1,2 @@ +mod mock_querier; +mod tests; diff --git a/contracts/router/src/testing/tests.rs b/contracts/router/src/testing/tests.rs new file mode 100644 index 0000000..18b1d13 --- /dev/null +++ b/contracts/router/src/testing/tests.rs @@ -0,0 +1,465 @@ +use cosmwasm_std::testing::{mock_env, mock_info, MOCK_CONTRACT_ADDR}; +use cosmwasm_std::{from_json, to_json_binary, Addr, Coin, ReplyOn, SubMsg, Uint128, WasmMsg}; +use cw20::{Cw20ExecuteMsg, Cw20ReceiveMsg}; + +use palomadex::asset::{native_asset_info, AssetInfo}; +use palomadex::router::{ + ConfigResponse, Cw20HookMsg, ExecuteMsg, InstantiateMsg, QueryMsg, + SimulateSwapOperationsResponse, SwapOperation, MAX_SWAP_OPERATIONS, +}; + +use crate::contract::{execute, instantiate, query, AFTER_SWAP_REPLY_ID}; +use crate::error::ContractError; +use crate::testing::mock_querier::mock_dependencies; + +#[test] +fn proper_initialization() { + let mut deps = mock_dependencies(&[]); + + let msg = InstantiateMsg { + palomadex_factory: String::from("palomadexfactory"), + }; + + let env = mock_env(); + let info = mock_info("addr0000", &[]); + + // We can just call .unwrap() to assert this was a success + let _res = instantiate(deps.as_mut(), env.clone(), info, msg).unwrap(); + + // It worked, let's query the state + let config: ConfigResponse = + from_json(&query(deps.as_ref(), env, QueryMsg::Config {}).unwrap()).unwrap(); + assert_eq!("palomadexfactory", config.palomadex_factory.as_str()); +} + +#[test] +fn execute_swap_operations() { + let mut deps = mock_dependencies(&[]); + let msg = InstantiateMsg { + palomadex_factory: String::from("palomadexfactory"), + }; + + let env = mock_env(); + let info = mock_info("addr0000", &[]); + + // We can just call .unwrap() to assert this was a success + let _res = instantiate(deps.as_mut(), env, info, msg).unwrap(); + + let msg = ExecuteMsg::ExecuteSwapOperations { + operations: vec![], + minimum_receive: None, + to: None, + max_spread: None, + }; + + let env = mock_env(); + let info = mock_info("addr0000", &[]); + let res = execute(deps.as_mut(), env, info, msg).unwrap_err(); + assert_eq!(res, ContractError::MustProvideOperations {}); + + let msg = ExecuteMsg::ExecuteSwapOperations { + operations: vec![ + SwapOperation::AstroSwap { + offer_asset_info: AssetInfo::NativeToken { + denom: "ukrw".to_string(), + }, + ask_asset_info: AssetInfo::Token { + contract_addr: Addr::unchecked("asset0001"), + }, + }, + SwapOperation::AstroSwap { + offer_asset_info: AssetInfo::Token { + contract_addr: Addr::unchecked("asset0001"), + }, + ask_asset_info: AssetInfo::NativeToken { + denom: "uluna".to_string(), + }, + }, + SwapOperation::AstroSwap { + offer_asset_info: AssetInfo::NativeToken { + denom: "uluna".to_string(), + }, + ask_asset_info: AssetInfo::Token { + contract_addr: Addr::unchecked("asset0002"), + }, + }, + ], + minimum_receive: Some(Uint128::from(1000000u128)), + to: None, + max_spread: None, + }; + + let env = mock_env(); + let info = mock_info("addr0000", &[]); + let res = execute(deps.as_mut(), env, info, msg).unwrap(); + assert_eq!( + res.messages, + vec![ + SubMsg { + msg: WasmMsg::Execute { + contract_addr: String::from(MOCK_CONTRACT_ADDR), + funds: vec![], + msg: to_json_binary(&ExecuteMsg::ExecuteSwapOperation { + operation: SwapOperation::AstroSwap { + offer_asset_info: AssetInfo::NativeToken { + denom: "ukrw".to_string(), + }, + ask_asset_info: AssetInfo::Token { + contract_addr: Addr::unchecked("asset0001"), + }, + }, + to: None, + max_spread: None, + single: false + }) + .unwrap(), + } + .into(), + id: 0, + gas_limit: None, + reply_on: ReplyOn::Never, + }, + SubMsg { + msg: WasmMsg::Execute { + contract_addr: String::from(MOCK_CONTRACT_ADDR), + funds: vec![], + msg: to_json_binary(&ExecuteMsg::ExecuteSwapOperation { + operation: SwapOperation::AstroSwap { + offer_asset_info: AssetInfo::Token { + contract_addr: Addr::unchecked("asset0001"), + }, + ask_asset_info: AssetInfo::NativeToken { + denom: "uluna".to_string(), + }, + }, + to: None, + max_spread: None, + single: false + }) + .unwrap(), + } + .into(), + id: 0, + gas_limit: None, + reply_on: ReplyOn::Never, + }, + SubMsg { + msg: WasmMsg::Execute { + contract_addr: String::from(MOCK_CONTRACT_ADDR), + funds: vec![], + msg: to_json_binary(&ExecuteMsg::ExecuteSwapOperation { + operation: SwapOperation::AstroSwap { + offer_asset_info: AssetInfo::NativeToken { + denom: "uluna".to_string(), + }, + ask_asset_info: AssetInfo::Token { + contract_addr: Addr::unchecked("asset0002"), + }, + }, + to: Some(String::from("addr0000")), + max_spread: None, + single: false + }) + .unwrap(), + } + .into(), + id: AFTER_SWAP_REPLY_ID, + gas_limit: None, + reply_on: ReplyOn::Success, + } + ] + ); + + let msg = ExecuteMsg::Receive(Cw20ReceiveMsg { + sender: String::from("addr0000"), + amount: Uint128::from(1000000u128), + msg: to_json_binary(&Cw20HookMsg::ExecuteSwapOperations { + operations: vec![ + SwapOperation::AstroSwap { + offer_asset_info: AssetInfo::NativeToken { + denom: "ukrw".to_string(), + }, + ask_asset_info: AssetInfo::Token { + contract_addr: Addr::unchecked("asset0001"), + }, + }, + SwapOperation::AstroSwap { + offer_asset_info: AssetInfo::Token { + contract_addr: Addr::unchecked("asset0001"), + }, + ask_asset_info: AssetInfo::NativeToken { + denom: "uluna".to_string(), + }, + }, + SwapOperation::AstroSwap { + offer_asset_info: AssetInfo::NativeToken { + denom: "uluna".to_string(), + }, + ask_asset_info: AssetInfo::Token { + contract_addr: Addr::unchecked("asset0002"), + }, + }, + ], + minimum_receive: None, + to: Some(String::from("addr0002")), + max_spread: None, + }) + .unwrap(), + }); + + let env = mock_env(); + let info = mock_info("asset0000", &[]); + let res = execute(deps.as_mut(), env, info, msg).unwrap(); + assert_eq!( + res.messages, + vec![ + SubMsg { + msg: WasmMsg::Execute { + contract_addr: String::from(MOCK_CONTRACT_ADDR), + funds: vec![], + msg: to_json_binary(&ExecuteMsg::ExecuteSwapOperation { + operation: SwapOperation::AstroSwap { + offer_asset_info: AssetInfo::NativeToken { + denom: "ukrw".to_string(), + }, + ask_asset_info: AssetInfo::Token { + contract_addr: Addr::unchecked("asset0001"), + }, + }, + to: None, + max_spread: None, + single: false + }) + .unwrap(), + } + .into(), + id: 0, + gas_limit: None, + reply_on: ReplyOn::Never, + }, + SubMsg { + msg: WasmMsg::Execute { + contract_addr: String::from(MOCK_CONTRACT_ADDR), + funds: vec![], + msg: to_json_binary(&ExecuteMsg::ExecuteSwapOperation { + operation: SwapOperation::AstroSwap { + offer_asset_info: AssetInfo::Token { + contract_addr: Addr::unchecked("asset0001"), + }, + ask_asset_info: AssetInfo::NativeToken { + denom: "uluna".to_string(), + }, + }, + to: None, + max_spread: None, + single: false + }) + .unwrap(), + } + .into(), + id: 0, + gas_limit: None, + reply_on: ReplyOn::Never, + }, + SubMsg { + msg: WasmMsg::Execute { + contract_addr: String::from(MOCK_CONTRACT_ADDR), + funds: vec![], + msg: to_json_binary(&ExecuteMsg::ExecuteSwapOperation { + operation: SwapOperation::AstroSwap { + offer_asset_info: AssetInfo::NativeToken { + denom: "uluna".to_string(), + }, + ask_asset_info: AssetInfo::Token { + contract_addr: Addr::unchecked("asset0002"), + }, + }, + to: Some(String::from("addr0002")), + max_spread: None, + single: false + }) + .unwrap(), + } + .into(), + id: AFTER_SWAP_REPLY_ID, + gas_limit: None, + reply_on: ReplyOn::Success, + } + ] + ); +} + +#[test] +fn execute_swap_operation() { + let mut deps = mock_dependencies(&[]); + let msg = InstantiateMsg { + palomadex_factory: String::from("palomadexfactory"), + }; + + let env = mock_env(); + let info = mock_info("addr0000", &[]); + + // We can just call .unwrap() to assert this was a success + let _res = instantiate(deps.as_mut(), env, info, msg).unwrap(); + + deps.querier + .with_palomadex_pairs(&[(&"uusdasset".to_string(), &String::from("pair"))]); + deps.querier.with_balance(&[( + &String::from(MOCK_CONTRACT_ADDR), + &[Coin { + amount: Uint128::new(1000000u128), + denom: "uusd".to_string(), + }], + )]); + + deps.querier + .with_palomadex_pairs(&[(&"assetuusd".to_string(), &String::from("pair"))]); + deps.querier.with_token_balances(&[( + &String::from("asset"), + &[( + &String::from(MOCK_CONTRACT_ADDR), + &Uint128::new(1000000u128), + )], + )]); + let msg = ExecuteMsg::ExecuteSwapOperation { + operation: SwapOperation::AstroSwap { + offer_asset_info: AssetInfo::Token { + contract_addr: Addr::unchecked("asset"), + }, + ask_asset_info: AssetInfo::NativeToken { + denom: "uusd".to_string(), + }, + }, + to: Some(String::from("addr0000")), + max_spread: None, + single: true, + }; + let env = mock_env(); + let info = mock_info(MOCK_CONTRACT_ADDR, &[]); + let res = execute(deps.as_mut(), env, info, msg).unwrap(); + assert_eq!( + res.messages, + vec![SubMsg { + msg: WasmMsg::Execute { + contract_addr: String::from("asset"), + funds: vec![], + msg: to_json_binary(&Cw20ExecuteMsg::Send { + contract: String::from("pair"), + amount: Uint128::new(1000000u128), + msg: to_json_binary(&palomadex::pair::Cw20HookMsg::Swap { + ask_asset_info: Some(native_asset_info("uusd".to_string())), + belief_price: None, + max_spread: None, + to: Some(String::from("addr0000")), + }) + .unwrap() + }) + .unwrap() + } + .into(), + id: 0, + gas_limit: None, + reply_on: ReplyOn::Never, + }] + ); +} + +#[test] +fn query_buy_with_routes() { + let mut deps = mock_dependencies(&[]); + + let msg = InstantiateMsg { + palomadex_factory: String::from("palomadexfactory"), + }; + + let env = mock_env(); + let info = mock_info("addr0000", &[]); + + // We can just call .unwrap() to assert this was a success + let _res = instantiate(deps.as_mut(), env.clone(), info, msg).unwrap(); + + let msg = QueryMsg::SimulateSwapOperations { + offer_amount: Uint128::from(1000000u128), + operations: vec![ + SwapOperation::AstroSwap { + offer_asset_info: AssetInfo::NativeToken { + denom: "ukrw".to_string(), + }, + ask_asset_info: AssetInfo::Token { + contract_addr: Addr::unchecked("asset0000"), + }, + }, + SwapOperation::AstroSwap { + offer_asset_info: AssetInfo::Token { + contract_addr: Addr::unchecked("asset0000"), + }, + ask_asset_info: AssetInfo::NativeToken { + denom: "uluna".to_string(), + }, + }, + ], + }; + deps.querier.with_palomadex_pairs(&[ + (&"ukrwasset0000".to_string(), &String::from("pair0000")), + (&"asset0000uluna".to_string(), &String::from("pair0001")), + ]); + + let res: SimulateSwapOperationsResponse = + from_json(&query(deps.as_ref(), env.clone(), msg).unwrap()).unwrap(); + assert_eq!( + res, + SimulateSwapOperationsResponse { + amount: Uint128::from(1000000u128) + } + ); + + assert_eq!( + res, + SimulateSwapOperationsResponse { + amount: Uint128::from(1000000u128), + } + ); + + let msg = QueryMsg::SimulateSwapOperations { + offer_amount: Uint128::from(1000000u128), + operations: vec![SwapOperation::NativeSwap { + offer_denom: "ukrw".to_string(), + ask_denom: "test".to_string(), + }], + }; + let err = query(deps.as_ref(), env.clone(), msg).unwrap_err(); + assert_eq!(err, ContractError::NativeSwapNotSupported {}); +} + +#[test] +fn assert_maximum_receive_swap_operations() { + let mut deps = mock_dependencies(&[]); + let msg = InstantiateMsg { + palomadex_factory: String::from("palomadexfactory"), + }; + + let env = mock_env(); + let info = mock_info("addr0000", &[]); + + // We can just call .unwrap() to assert this was a success + let _res = instantiate(deps.as_mut(), env, info, msg).unwrap(); + + let msg = ExecuteMsg::ExecuteSwapOperations { + operations: vec![ + SwapOperation::NativeSwap { + offer_denom: "uusd".to_string(), + ask_denom: "ukrw".to_string(), + }; + MAX_SWAP_OPERATIONS + 1 + ], + minimum_receive: None, + to: None, + max_spread: None, + }; + + let env = mock_env(); + let info = mock_info("addr0000", &[]); + let res = execute(deps.as_mut(), env, info, msg).unwrap_err(); + + assert_eq!(res, ContractError::SwapLimitExceeded {}); +} diff --git a/contracts/router/tests/factory_helper.rs b/contracts/router/tests/factory_helper.rs new file mode 100644 index 0000000..94d90b2 --- /dev/null +++ b/contracts/router/tests/factory_helper.rs @@ -0,0 +1,188 @@ +#![cfg(not(tarpaulin_include))] + +use anyhow::Result as AnyResult; +use cosmwasm_std::{coins, Addr, Binary}; +use cw20::MinterResponse; +use cw_multi_test::{App, AppResponse, ContractWrapper, Executor}; + +use palomadex::asset::{AssetInfo, PairInfo}; +use palomadex::factory::{PairConfig, PairType, QueryMsg}; + +pub struct FactoryHelper { + // pub owner: Addr, + pub factory: Addr, + pub cw20_token_code_id: u64, +} + +impl FactoryHelper { + pub fn init(router: &mut App, owner: &Addr) -> Self { + let astro_token_contract = Box::new(ContractWrapper::new_with_empty( + cw20_base::contract::execute, + cw20_base::contract::instantiate, + cw20_base::contract::query, + )); + + let cw20_token_code_id = router.store_code(astro_token_contract); + + let pair_contract = Box::new( + ContractWrapper::new_with_empty( + palomadex_pair::contract::execute, + palomadex_pair::contract::instantiate, + palomadex_pair::contract::query, + ) + .with_reply_empty(palomadex_pair::contract::reply), + ); + + let pair_code_id = router.store_code(pair_contract); + + let factory_contract = Box::new( + ContractWrapper::new_with_empty( + palomadex_factory::contract::execute, + palomadex_factory::contract::instantiate, + palomadex_factory::contract::query, + ) + .with_reply_empty(palomadex_factory::contract::reply), + ); + + let factory_code_id = router.store_code(factory_contract); + + let msg = palomadex::factory::InstantiateMsg { + pair_configs: vec![ + PairConfig { + code_id: pair_code_id, + pair_type: PairType::Xyk {}, + total_fee_bps: 0, + maker_fee_bps: 0, + is_disabled: false, + is_generator_disabled: false, + permissioned: false, + }, + PairConfig { + code_id: pair_code_id, + pair_type: PairType::Stable {}, + total_fee_bps: 0, + maker_fee_bps: 0, + is_disabled: false, + is_generator_disabled: false, + permissioned: false, + }, + ], + token_code_id: cw20_token_code_id, + fee_address: None, + generator_address: None, + owner: owner.to_string(), + whitelist_code_id: 0, + coin_registry_address: "coin_registry".to_string(), + }; + + let factory = router + .instantiate_contract( + factory_code_id, + owner.clone(), + &msg, + &[], + String::from("ASTRO"), + None, + ) + .unwrap(); + + Self { + // owner: owner.clone(), + factory, + cw20_token_code_id, + } + } + + pub fn create_pair( + &mut self, + router: &mut App, + sender: &Addr, + pair_type: PairType, + asset_infos: [AssetInfo; 2], + init_params: Option, + ) -> AnyResult { + let msg = palomadex::factory::ExecuteMsg::CreatePair { + pair_type, + asset_infos: asset_infos.to_vec(), + init_params, + }; + + router.execute_contract(sender.clone(), self.factory.clone(), &msg, &[])?; + + let res: PairInfo = router.wrap().query_wasm_smart( + self.factory.clone(), + &QueryMsg::Pair { + asset_infos: asset_infos.to_vec(), + }, + )?; + + Ok(res.contract_addr) + } +} + +pub fn instantiate_token( + app: &mut App, + token_code_id: u64, + owner: &Addr, + token_name: &str, + decimals: Option, +) -> Addr { + let init_msg = palomadex::token::InstantiateMsg { + name: token_name.to_string(), + symbol: token_name.to_string(), + decimals: decimals.unwrap_or(6), + initial_balances: vec![], + mint: Some(MinterResponse { + minter: owner.to_string(), + cap: None, + }), + marketing: None, + }; + + app.instantiate_contract( + token_code_id, + owner.clone(), + &init_msg, + &[], + token_name, + None, + ) + .unwrap() +} + +pub fn mint( + app: &mut App, + owner: &Addr, + token: &Addr, + amount: u128, + receiver: &Addr, +) -> AnyResult { + app.execute_contract( + owner.clone(), + token.clone(), + &cw20::Cw20ExecuteMsg::Mint { + recipient: receiver.to_string(), + amount: amount.into(), + }, + &[], + ) +} + +pub fn mint_native( + app: &mut App, + denom: &str, + amount: u128, + receiver: &Addr, +) -> AnyResult { + // .init_balance() erases previous balance thus we use such hack and create intermediate "denom admin" + let denom_admin = Addr::unchecked(format!("{denom}_admin")); + let coins_vec = coins(amount, denom); + app.init_modules(|router, _, storage| { + router + .bank + .init_balance(storage, &denom_admin, coins_vec.clone()) + }) + .unwrap(); + + app.send_tokens(denom_admin, receiver.clone(), &coins_vec) +} diff --git a/contracts/router/tests/router_integration.rs b/contracts/router/tests/router_integration.rs new file mode 100644 index 0000000..3b12d96 --- /dev/null +++ b/contracts/router/tests/router_integration.rs @@ -0,0 +1,888 @@ +#![cfg(not(tarpaulin_include))] + +use cosmwasm_std::{coins, from_json, to_json_binary, Addr, Empty, StdError}; +use cw20::Cw20ExecuteMsg; +use cw_multi_test::{App, Contract, ContractWrapper, Executor}; + +use palomadex::asset::{native_asset_info, token_asset_info}; +use palomadex::factory::PairType; +use palomadex::router::{ExecuteMsg, InstantiateMsg, SwapOperation, SwapResponseData}; +use palomadex_router::error::ContractError; + +use crate::factory_helper::{instantiate_token, mint, mint_native, FactoryHelper}; + +mod factory_helper; + +fn router_contract() -> Box> { + Box::new( + ContractWrapper::new_with_empty( + palomadex_router::contract::execute, + palomadex_router::contract::instantiate, + palomadex_router::contract::query, + ) + .with_reply_empty(palomadex_router::contract::reply), + ) +} + +#[test] +fn router_does_not_enforce_spread_assertion() { + let mut app = App::default(); + + let owner = Addr::unchecked("owner"); + let mut helper = FactoryHelper::init(&mut app, &owner); + + let token_x = instantiate_token(&mut app, helper.cw20_token_code_id, &owner, "TOX", None); + let token_y = instantiate_token(&mut app, helper.cw20_token_code_id, &owner, "TOY", None); + let token_z = instantiate_token(&mut app, helper.cw20_token_code_id, &owner, "TOZ", None); + + for (a, b, typ, liq) in [ + (&token_x, &token_y, PairType::Xyk {}, 100_000_000000), + (&token_y, &token_z, PairType::Stable {}, 1_000_000_000000), + ] { + let pair = helper + .create_pair( + &mut app, + &owner, + typ, + [token_asset_info(a.clone()), token_asset_info(b.clone())], + None, + ) + .unwrap(); + mint(&mut app, &owner, a, liq, &pair).unwrap(); + mint(&mut app, &owner, b, liq, &pair).unwrap(); + } + + let router_code = app.store_code(router_contract()); + let router = app + .instantiate_contract( + router_code, + owner.clone(), + &InstantiateMsg { + palomadex_factory: helper.factory.to_string(), + }, + &[], + "router", + None, + ) + .unwrap(); + + // Triggering swap with a huge spread fees + mint(&mut app, &owner, &token_x, 50_000_000000, &owner).unwrap(); + let resp = app + .execute_contract( + owner.clone(), + token_x.clone(), + &Cw20ExecuteMsg::Send { + contract: router.to_string(), + amount: 50_000_000000u128.into(), + msg: to_json_binary(&ExecuteMsg::ExecuteSwapOperations { + operations: vec![ + SwapOperation::AstroSwap { + offer_asset_info: token_asset_info(token_x.clone()), + ask_asset_info: token_asset_info(token_y.clone()), + }, + SwapOperation::AstroSwap { + offer_asset_info: token_asset_info(token_y.clone()), + ask_asset_info: token_asset_info(token_z.clone()), + }, + ], + minimum_receive: None, + to: None, + max_spread: None, + }) + .unwrap(), + }, + &[], + ) + .unwrap(); + + // We can't set data in response if the first message dispatched from cw20 contract + assert!( + resp.data.is_none(), + "Unexpected data set after cw20 send hook" + ); + + // However, single hop will still enforce spread assertion + mint(&mut app, &owner, &token_x, 50_000_000000, &owner).unwrap(); + let err = app + .execute_contract( + owner.clone(), + token_x.clone(), + &Cw20ExecuteMsg::Send { + contract: router.to_string(), + amount: 50_000_000000u128.into(), + msg: to_json_binary(&ExecuteMsg::ExecuteSwapOperations { + operations: vec![SwapOperation::AstroSwap { + offer_asset_info: token_asset_info(token_x.clone()), + ask_asset_info: token_asset_info(token_y.clone()), + }], + minimum_receive: None, + to: None, + max_spread: None, + }) + .unwrap(), + }, + &[], + ) + .unwrap_err(); + assert_eq!( + palomadex_pair::error::ContractError::MaxSpreadAssertion {}, + err.downcast().unwrap() + ) +} + +#[test] +fn route_through_pairs_with_natives() { + let mut app = App::default(); + + let owner = Addr::unchecked("owner"); + let mut helper = FactoryHelper::init(&mut app, &owner); + + let denom_x = "denom_x"; + let denom_y = "denom_y"; + let denom_z = "denom_z"; + + for (a, b, typ, liq) in [ + (&denom_x, &denom_y, PairType::Xyk {}, 100_000_000000), + (&denom_y, &denom_z, PairType::Stable {}, 1_000_000_000000), + ] { + let pair = helper + .create_pair( + &mut app, + &owner, + typ, + [ + native_asset_info(a.to_string()), + native_asset_info(b.to_string()), + ], + None, + ) + .unwrap(); + mint_native(&mut app, a, liq, &pair).unwrap(); + mint_native(&mut app, b, liq, &pair).unwrap(); + } + + let router_code = app.store_code(router_contract()); + let router = app + .instantiate_contract( + router_code, + owner.clone(), + &InstantiateMsg { + palomadex_factory: helper.factory.to_string(), + }, + &[], + "router", + None, + ) + .unwrap(); + + // Sanity checks + + let err = app + .execute_contract( + owner.clone(), + router.clone(), + &ExecuteMsg::ExecuteSwapOperation { + operation: SwapOperation::AstroSwap { + offer_asset_info: native_asset_info(denom_x.to_string()), + ask_asset_info: native_asset_info(denom_y.to_string()), + }, + to: None, + max_spread: None, + single: false, + }, + &[], + ) + .unwrap_err(); + assert_eq!( + err.downcast::().unwrap(), + ContractError::Unauthorized {} + ); + let err = app + .execute_contract( + owner.clone(), + router.clone(), + &ExecuteMsg::ExecuteSwapOperations { + operations: vec![SwapOperation::NativeSwap { + offer_denom: denom_x.to_string(), + ask_denom: denom_y.to_string(), + }], + to: None, + max_spread: None, + minimum_receive: None, + }, + &[], + ) + .unwrap_err(); + assert_eq!( + err.downcast::().unwrap(), + ContractError::NativeSwapNotSupported {} + ); + + let err = app + .execute_contract( + owner.clone(), + router.clone(), + &ExecuteMsg::ExecuteSwapOperations { + operations: vec![SwapOperation::AstroSwap { + offer_asset_info: native_asset_info(denom_x.to_string()), + ask_asset_info: native_asset_info(denom_x.to_string()), + }], + to: None, + max_spread: None, + minimum_receive: None, + }, + &[], + ) + .unwrap_err(); + assert_eq!( + err.downcast::().unwrap(), + ContractError::DoublingAssetsPath { + offer_asset: denom_x.to_string(), + ask_asset: denom_x.to_string() + } + ); + + // End sanity checks + + mint_native(&mut app, &denom_x, 50_000_000000, &owner).unwrap(); + let resp = app + .execute_contract( + owner.clone(), + router.clone(), + &ExecuteMsg::ExecuteSwapOperations { + operations: vec![ + SwapOperation::AstroSwap { + offer_asset_info: native_asset_info(denom_x.to_string()), + ask_asset_info: native_asset_info(denom_y.to_string()), + }, + SwapOperation::AstroSwap { + offer_asset_info: native_asset_info(denom_y.to_string()), + ask_asset_info: native_asset_info(denom_z.to_string()), + }, + ], + minimum_receive: None, + to: None, + max_spread: None, + }, + &coins(50_000_000000, denom_x), + ) + .unwrap(); + + let resp_data: SwapResponseData = from_json(&resp.data.unwrap()).unwrap(); + + assert_eq!(resp_data.return_amount.u128(), 32_258_064517); + + mint_native(&mut app, &denom_x, 50_000_000000, &owner).unwrap(); + let err = app + .execute_contract( + owner.clone(), + router, + &ExecuteMsg::ExecuteSwapOperations { + operations: vec![ + SwapOperation::AstroSwap { + offer_asset_info: native_asset_info(denom_x.to_string()), + ask_asset_info: native_asset_info(denom_y.to_string()), + }, + SwapOperation::AstroSwap { + offer_asset_info: native_asset_info(denom_y.to_string()), + ask_asset_info: native_asset_info(denom_z.to_string()), + }, + ], + minimum_receive: Some(50_000_000000u128.into()), // <--- enforcing minimum receive with 1:1 rate (which practically impossible) + to: None, + max_spread: None, + }, + &coins(50_000_000000, denom_x), + ) + .unwrap_err(); + + assert_eq!( + err.downcast::().unwrap(), + ContractError::AssertionMinimumReceive { + receive: 50_000_000000u128.into(), + amount: 15_360_983104u128.into() + } + ); +} + +#[test] +fn test_swap_route() { + use crate::factory_helper::{instantiate_token, mint, FactoryHelper}; + use cosmwasm_std::{to_json_binary, Addr, Uint128}; + use cw20::{BalanceResponse, Cw20ExecuteMsg, Cw20QueryMsg}; + use palomadex::asset::AssetInfo; + use palomadex::factory::PairType; + use palomadex::router::{ + ExecuteMsg, InstantiateMsg, QueryMsg, SimulateSwapOperationsResponse, SwapOperation, + }; + let mut app = App::default(); + let owner = Addr::unchecked("owner"); + let mut helper = FactoryHelper::init(&mut app, &owner); + let astro = instantiate_token(&mut app, helper.cw20_token_code_id, &owner, "astro", None); + let inj = instantiate_token(&mut app, helper.cw20_token_code_id, &owner, "inj", None); + let atom = instantiate_token(&mut app, helper.cw20_token_code_id, &owner, "atom", None); + let osmo = instantiate_token(&mut app, helper.cw20_token_code_id, &owner, "osmo", None); + + for (a, b, typ, liq) in [ + (&astro, &inj, PairType::Xyk {}, 100_000_000000), + (&inj, &osmo, PairType::Xyk {}, 100_000_000000), + (&atom, &osmo, PairType::Xyk {}, 100_000_000000), + ] { + let pair = helper + .create_pair( + &mut app, + &owner, + typ, + [token_asset_info(a.clone()), token_asset_info(b.clone())], + None, + ) + .unwrap(); + mint(&mut app, &owner, a, liq, &pair).unwrap(); + mint(&mut app, &owner, b, liq, &pair).unwrap(); + } + let router_code = app.store_code(router_contract()); + let router = app + .instantiate_contract( + router_code, + owner.clone(), + &InstantiateMsg { + palomadex_factory: helper.factory.to_string(), + }, + &[], + "router", + None, + ) + .unwrap(); + + let swap_amount = Uint128::new(10_000_000); + + // Try to swap with a bad batch of path + // route: astro -> inj, atom -> osmo + let swap_operations = vec![ + SwapOperation::AstroSwap { + offer_asset_info: AssetInfo::Token { + contract_addr: astro.clone(), + }, + ask_asset_info: AssetInfo::Token { + contract_addr: inj.clone(), + }, + }, + SwapOperation::AstroSwap { + offer_asset_info: AssetInfo::Token { + contract_addr: atom.clone(), + }, + ask_asset_info: AssetInfo::Token { + contract_addr: osmo.clone(), + }, + }, + ]; + + let err = app + .wrap() + .query_wasm_smart::( + router.clone(), + &QueryMsg::SimulateSwapOperations { + offer_amount: swap_amount, + operations: swap_operations.clone(), + }, + ) + .unwrap_err(); + assert_eq!( + err, + StdError::generic_err( + "Querier contract error: The next offer asset must be \ + the same as the previous ask asset; contract2 --> contract3 --> contract4" + ) + ); + + // swap astro for osmo + // route: astro -> inj, inj -> osmo, osmo -> atom, atom -> osmo + let swap_operations = vec![ + SwapOperation::AstroSwap { + offer_asset_info: AssetInfo::Token { + contract_addr: astro.clone(), + }, + ask_asset_info: AssetInfo::Token { + contract_addr: inj.clone(), + }, + }, + SwapOperation::AstroSwap { + offer_asset_info: AssetInfo::Token { + contract_addr: inj.clone(), + }, + ask_asset_info: AssetInfo::Token { + contract_addr: osmo.clone(), + }, + }, + SwapOperation::AstroSwap { + offer_asset_info: AssetInfo::Token { + contract_addr: osmo.clone(), + }, + ask_asset_info: AssetInfo::Token { + contract_addr: atom.clone(), + }, + }, + SwapOperation::AstroSwap { + offer_asset_info: AssetInfo::Token { + contract_addr: atom.clone(), + }, + ask_asset_info: AssetInfo::Token { + contract_addr: osmo.clone(), + }, + }, + ]; + + // the simulation succeeds + let simulate_res: SimulateSwapOperationsResponse = app + .wrap() + .query_wasm_smart( + router.clone(), + &QueryMsg::SimulateSwapOperations { + offer_amount: swap_amount, + operations: swap_operations.clone(), + }, + ) + .unwrap(); + + assert_eq!(simulate_res.amount, Uint128::new(9996004)); + println!( + "0. User simulate swap, expected return amount: {:?}", + simulate_res.amount + ); + + let user = Addr::unchecked("user"); + mint(&mut app, &owner, &astro, swap_amount.u128(), &user).unwrap(); + + // query balance + let balance_res: BalanceResponse = app + .wrap() + .query_wasm_smart( + astro.clone(), + &Cw20QueryMsg::Balance { + address: user.to_string(), + }, + ) + .unwrap(); + assert_eq!(balance_res.balance, swap_amount); + + // swap + app.execute_contract( + user.clone(), + astro.clone(), + &Cw20ExecuteMsg::Send { + contract: router.to_string(), + amount: swap_amount, + msg: to_json_binary(&ExecuteMsg::ExecuteSwapOperations { + operations: swap_operations.clone(), + minimum_receive: None, + to: None, + max_spread: None, + }) + .unwrap(), + }, + &[], + ) + .unwrap(); + + let attacker = Addr::unchecked("attacker"); + let donated_atom: u128 = 1; + + mint(&mut app, &owner, &atom, donated_atom, &attacker).unwrap(); + + // attacker donates little amount to router contract + app.execute_contract( + attacker.clone(), + atom.clone(), + &Cw20ExecuteMsg::Transfer { + recipient: router.to_string(), + amount: Uint128::new(donated_atom), + }, + &[], + ) + .unwrap(); + + // query balance + let balance_res: BalanceResponse = app + .wrap() + .query_wasm_smart( + atom.clone(), + &Cw20QueryMsg::Balance { + address: router.to_string(), + }, + ) + .unwrap(); + assert_eq!(balance_res.balance, Uint128::new(1)); + + // query balance + let balance_res: BalanceResponse = app + .wrap() + .query_wasm_smart( + astro.clone(), + &Cw20QueryMsg::Balance { + address: user.to_string(), + }, + ) + .unwrap(); + assert_eq!(balance_res.balance, Uint128::zero()); + + // query balance + let balance_res: BalanceResponse = app + .wrap() + .query_wasm_smart( + osmo.clone(), + &Cw20QueryMsg::Balance { + address: user.to_string(), + }, + ) + .unwrap(); + assert_eq!(balance_res.balance, Uint128::new(9998003)); + + // query balance + let balance_res: BalanceResponse = app + .wrap() + .query_wasm_smart( + osmo.clone(), + &Cw20QueryMsg::Balance { + address: router.to_string(), + }, + ) + .unwrap(); + assert_eq!(balance_res.balance, Uint128::zero()); + + // mint more astro to user + mint(&mut app, &owner, &astro, swap_amount.u128(), &user).unwrap(); + + // victim tx gets executed. Assume user provide `minimum_receive` as `None`" + app.execute_contract( + user.clone(), + astro.clone(), + &Cw20ExecuteMsg::Send { + contract: router.to_string(), + amount: swap_amount, + msg: to_json_binary(&ExecuteMsg::ExecuteSwapOperations { + operations: swap_operations.clone(), + minimum_receive: None, + to: None, + max_spread: None, + }) + .unwrap(), + }, + &[], + ) + .unwrap(); + + // Query victim balance + let balance_res: BalanceResponse = app + .wrap() + .query_wasm_smart( + astro.clone(), + &Cw20QueryMsg::Balance { + address: user.to_string(), + }, + ) + .unwrap(); + assert_eq!(balance_res.balance, Uint128::zero()); + + let balance_res: BalanceResponse = app + .wrap() + .query_wasm_smart( + atom.clone(), + &Cw20QueryMsg::Balance { + address: user.to_string(), + }, + ) + .unwrap(); + assert_eq!(balance_res.balance, Uint128::zero()); + + let balance_res: BalanceResponse = app + .wrap() + .query_wasm_smart( + osmo.clone(), + &Cw20QueryMsg::Balance { + address: user.to_string(), + }, + ) + .unwrap(); + assert_eq!(balance_res.balance, Uint128::new(19992009)); + + // Query router contract balance + let balance_res: BalanceResponse = app + .wrap() + .query_wasm_smart( + astro.clone(), + &Cw20QueryMsg::Balance { + address: router.to_string(), + }, + ) + .unwrap(); + assert_eq!(balance_res.balance, Uint128::zero()); + + let balance_res: BalanceResponse = app + .wrap() + .query_wasm_smart( + atom.clone(), + &Cw20QueryMsg::Balance { + address: router.to_string(), + }, + ) + .unwrap(); + assert_eq!(balance_res.balance, Uint128::zero()); + + let balance_res: BalanceResponse = app + .wrap() + .query_wasm_smart( + osmo.clone(), + &Cw20QueryMsg::Balance { + address: router.to_string(), + }, + ) + .unwrap(); + println!("OSMO router balance: {:?}", balance_res.balance); + + // attacker try back-runs the tx and withdraw nothing + let err = app + .execute_contract( + attacker.clone(), + router.clone(), + &ExecuteMsg::ExecuteSwapOperations { + operations: vec![SwapOperation::AstroSwap { + offer_asset_info: AssetInfo::Token { + contract_addr: osmo.clone(), + }, + ask_asset_info: AssetInfo::Token { + contract_addr: atom.clone(), + }, + }], + minimum_receive: Some(Uint128::new(9_997_000)), + to: None, + max_spread: None, + }, + &[], + ) + .unwrap_err(); + assert_eq!( + err.root_cause().to_string(), + "Generic error: Swap amount must not be zero" + ); + + // Query attacker balance and calculate profit + let balance_res: BalanceResponse = app + .wrap() + .query_wasm_smart( + astro.clone(), + &Cw20QueryMsg::Balance { + address: attacker.to_string(), + }, + ) + .unwrap(); + assert_eq!(balance_res.balance, Uint128::zero()); + + let balance_res: BalanceResponse = app + .wrap() + .query_wasm_smart( + atom.clone(), + &Cw20QueryMsg::Balance { + address: attacker.to_string(), + }, + ) + .unwrap(); + + println!("ATOM attacker balance: {:?}", balance_res.balance); + println!("Donated ATOM: {:?}", donated_atom); + + let profit = balance_res + .balance + .saturating_sub(Uint128::new(donated_atom)); + println!("Attacker's profit: {:?}", profit); + + let balance_res: BalanceResponse = app + .wrap() + .query_wasm_smart( + osmo.clone(), + &Cw20QueryMsg::Balance { + address: attacker.to_string(), + }, + ) + .unwrap(); + assert_eq!(balance_res.balance, Uint128::zero()); + + // double check router contract have no funds left + let balance_res: BalanceResponse = app + .wrap() + .query_wasm_smart( + astro.clone(), + &Cw20QueryMsg::Balance { + address: router.to_string(), + }, + ) + .unwrap(); + assert_eq!(balance_res.balance, Uint128::zero()); + + let balance_res: BalanceResponse = app + .wrap() + .query_wasm_smart( + atom.clone(), + &Cw20QueryMsg::Balance { + address: router.to_string(), + }, + ) + .unwrap(); + assert_eq!(balance_res.balance, Uint128::zero()); + + let balance_res: BalanceResponse = app + .wrap() + .query_wasm_smart( + osmo.clone(), + &Cw20QueryMsg::Balance { + address: router.to_string(), + }, + ) + .unwrap(); + assert_eq!(balance_res.balance, Uint128::zero()); + + /* ------------------------------------------------------------------------------------------- + 2. lets try attack with minimum_receive as Some(_). + -------------------------------------------------------------------------------------------*/ + println!("\n2. Assume user provide `minimum_receive` as `Some(_)`"); + + mint(&mut app, &owner, &astro, swap_amount.u128(), &user).unwrap(); + + // query balance + let balance_res: BalanceResponse = app + .wrap() + .query_wasm_smart( + astro.clone(), + &Cw20QueryMsg::Balance { + address: user.to_string(), + }, + ) + .unwrap(); + assert_eq!(balance_res.balance, swap_amount); + + // attacker2 front-run tx + let attacker2 = Addr::unchecked("attacker2"); + + // assume the market is bad and user wants to get as much as they can + let donated_atom = Uint128::new(9_000_000); + + // attacker2 donate funds + mint(&mut app, &owner, &atom, donated_atom.u128(), &attacker2).unwrap(); + + app.execute_contract( + attacker2.clone(), + atom.clone(), + &Cw20ExecuteMsg::Transfer { + recipient: router.to_string(), + amount: donated_atom, + }, + &[], + ) + .unwrap(); + + // victim tx gets executed + app.execute_contract( + user.clone(), + astro.clone(), + &Cw20ExecuteMsg::Send { + contract: router.to_string(), + amount: swap_amount, + msg: to_json_binary(&ExecuteMsg::ExecuteSwapOperations { + operations: swap_operations.clone(), + minimum_receive: Some(donated_atom), + to: None, + max_spread: None, + }) + .unwrap(), + }, + &[], + ) + .unwrap(); + + // query router contract + let balance_res: BalanceResponse = app + .wrap() + .query_wasm_smart( + astro.clone(), + &Cw20QueryMsg::Balance { + address: router.to_string(), + }, + ) + .unwrap(); + println!("ASTRO router balance: {:?}", balance_res.balance); + + let balance_res: BalanceResponse = app + .wrap() + .query_wasm_smart( + atom.clone(), + &Cw20QueryMsg::Balance { + address: router.to_string(), + }, + ) + .unwrap(); + println!("ATOM router balance: {:?}", balance_res.balance); + + let balance_res: BalanceResponse = app + .wrap() + .query_wasm_smart( + osmo.clone(), + &Cw20QueryMsg::Balance { + address: router.to_string(), + }, + ) + .unwrap(); + println!("OSMO router balance: {:?}", balance_res.balance); + + // attacker back-runs tx to withdraw funds + let err = app + .execute_contract( + attacker2.clone(), + router.clone(), + &ExecuteMsg::ExecuteSwapOperations { + operations: vec![SwapOperation::AstroSwap { + offer_asset_info: AssetInfo::Token { + contract_addr: osmo.clone(), + }, + ask_asset_info: AssetInfo::Token { + contract_addr: atom.clone(), + }, + }], + minimum_receive: None, + to: None, + max_spread: None, + }, + &[], + ) + .unwrap_err(); + assert_eq!( + err.root_cause().to_string(), + "Generic error: Swap amount must not be zero" + ); + + let balance_res: BalanceResponse = app + .wrap() + .query_wasm_smart( + astro.clone(), + &Cw20QueryMsg::Balance { + address: attacker2.to_string(), + }, + ) + .unwrap(); + assert_eq!(balance_res.balance, Uint128::zero()); + + let balance_res: BalanceResponse = app + .wrap() + .query_wasm_smart( + atom.clone(), + &Cw20QueryMsg::Balance { + address: attacker2.to_string(), + }, + ) + .unwrap(); + + println!("ATOM attacker2 balance: {:?}", balance_res.balance); + println!("Donated ATOM: {:?}", donated_atom); + + let profit = balance_res.balance.saturating_sub(donated_atom); + println!("Attacker2's profit: {:?}", profit); +} diff --git a/packages/basic_stableswap_sim/Cargo.toml b/packages/basic_stableswap_sim/Cargo.toml new file mode 100644 index 0000000..bbda019 --- /dev/null +++ b/packages/basic_stableswap_sim/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "basic-stableswap-sim" +version = "0.1.0" +authors = ["Volume Finanace"] +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +# features = ["auto-initialize"] + +[dependencies] +cosmwasm-std = { workspace = true } \ No newline at end of file diff --git a/packages/basic_stableswap_sim/src/lib.rs b/packages/basic_stableswap_sim/src/lib.rs new file mode 100644 index 0000000..a396627 --- /dev/null +++ b/packages/basic_stableswap_sim/src/lib.rs @@ -0,0 +1,371 @@ +use cosmwasm_std::Uint256; + +const DEFAULT_POOL_TOKENS: u128 = 0; +const DEFAULT_TARGET_PRICE: u128 = 1000000000000000000; +pub const MODEL_FEE_NUMERATOR: u128 = 1; +pub const MODEL_FEE_DENOMINATOR: u128 = 1000; + +pub struct StableSwapModel { + pub amp_factor: u128, + pub balances: Vec, + pub n_coins: u8, + pub fee: u128, + pub target_prices: Vec, + pub pool_tokens: u128, +} + +impl StableSwapModel { + pub fn new(amp_factor: u128, balances: Vec, n_coins: u8) -> StableSwapModel { + Self { + amp_factor, + balances, + n_coins, + fee: 0u128, + target_prices: vec![DEFAULT_TARGET_PRICE; n_coins as usize], + pool_tokens: DEFAULT_POOL_TOKENS, + } + } + + pub fn new_with_pool_tokens( + amp_factor: u128, + balances: Vec, + n_coins: u8, + pool_token_amount: u128, + ) -> StableSwapModel { + Self { + amp_factor, + balances, + n_coins, + fee: 0u128, + target_prices: vec![DEFAULT_TARGET_PRICE; n_coins as usize], + pool_tokens: pool_token_amount, + } + } + + fn xp(&self) -> Vec { + self.balances + .iter() + .zip(&self.target_prices) + .map(|(x, p)| x * p / 10u128.pow(18)) + .collect() + } + + // pub fn sim_d(&self) -> u128 { + // let gil = Python::acquire_gil(); + // return self + // .call0(gil.python(), "D") + // .unwrap() + // .extract(gil.python()) + // .unwrap(); + // } + + pub fn sim_d(&self) -> u128 { + self.d() + } + + fn d(&self) -> u128 { + let mut d_prev = 0; + let xp = self.xp(); + let s: u128 = xp.iter().sum(); + let mut d = s; + let ann = self.amp_factor * self.n_coins as u128; + + let mut counter = 0; + + while d.abs_diff(d_prev) > 1 { + let mut d_p = d; + for x in &xp { + // println!("d_p = {}", d_p); + // println!("d = {}", d); + // println!("self.n_coins = {}", self.n_coins); + // println!("x = {}", x); + d_p = (Uint256::from_u128(d_p) * Uint256::from_u128(d) + / Uint256::from_u128(self.n_coins as u128 * x)) + .to_string() + .parse::() + .unwrap(); + } + d_prev = d; + // println!("d_p = {}", d_p); + // println!("d = {}", d); + // println!("self.n_coins = {}", self.n_coins); + // println!("s = {}", s); + // println!("ann = {}", ann); + // println!("{}", ann * s + d_p * self.n_coins as u128); + + d = (Uint256::from_u128(d) * Uint256::from_u128(ann * s + d_p * self.n_coins as u128) + / Uint256::from_u128((ann - 1) * d + (self.n_coins as u128 + 1) * d_p)) + .to_string() + .parse::() + .unwrap(); + + counter += 1; + if counter > 1000 { + break; + } + } + + d + } + + fn y(&self, i: usize, j: usize, x: u128) -> u128 { + let d: u128 = self.d(); + let mut xx = self.xp(); + xx[i] = x; + xx.remove(j); + let ann = self.amp_factor * self.n_coins as u128; + let mut c = d; + for y in &xx { + c = c * d / (y * self.n_coins as u128); + } + c = c * d / (self.n_coins as u128 * ann); + let b: i128 = xx.iter().sum::() as i128 + (d / ann) as i128 - d as i128; + let mut y_prev = 0; + let mut y = d; + + let mut counter = 0; + + while y.abs_diff(y_prev) > 1 { + // println!("y_prev = {}", y_prev); + // println!("y = {}", y); + // println!("y pow 2 = {}", y.pow(2)); + // println!("c = {}", c); + // println!("b = {}", b); + // println!("counter = {}", counter); + y_prev = y; + y = (y.pow(2) + c) / (2 * y as i128 + b) as u128; + + counter += 1; + if counter > 1000 { + break; + } + } + + y + } + + fn dy(&self, i: usize, j: usize, dx: u128) -> u128 { + let xp = self.xp(); + xp[j] - self.y(i, j, xp[i] + dx) + } + + // pub fn sim_dy(&self, i: u128, j: u128, dx: u128) -> u128 { + // let gil = Python::acquire_gil(); + // return self + // .call1(gil.python(), "dy", (i, j, dx)) + // .unwrap() + // .extract(gil.python()) + // .unwrap(); + // } + + pub fn sim_dy(&self, i: u128, j: u128, dx: u128) -> u128 { + self.dy(i as usize, j as usize, dx) + } + + fn exchange(&mut self, i: usize, j: usize, dx: u128) -> u128 { + let xp = self.xp(); + let x = xp[i] + dx; + let y = self.y(i, j, x); + let dy = xp[j] - y; + let fee = dy * self.fee / 10u128.pow(10); + + if dy == 0 { + return 0; + } + + self.balances[i] = x * 10u128.pow(18) / self.target_prices[i]; + self.balances[j] = (y + fee) * 10u128.pow(18) / self.target_prices[j]; + dy - fee + } + + // pub fn sim_exchange(&self, i: u128, j: u128, dx: u128) -> u128 { + // let gil = Python::acquire_gil(); + // return self + // .call1(gil.python(), "exchange", (i, j, dx)) + // .unwrap() + // .extract(gil.python()) + // .unwrap(); + // } + + pub fn sim_exchange(&mut self, i: u128, j: u128, dx: u128) -> u128 { + self.exchange(i as usize, j as usize, dx) + } + + pub fn sim_xp(&self) -> Vec { + self.xp() + } + + // pub fn sim_xp(&self) -> Vec { + // let gil = Python::acquire_gil(); + // return self + // .call0(gil.python(), "xp") + // .unwrap() + // .extract(gil.python()) + // .unwrap(); + // } + + pub fn sim_y(&self, i: u128, j: u128, x: u128) -> u128 { + self.y(i as usize, j as usize, x) + } + + // pub fn sim_y(&self, i: u128, j: u128, x: u128) -> u128 { + // let gil = Python::acquire_gil(); + // return self + // .call1(gil.python(), "y", (i, j, x)) + // .unwrap() + // .extract(gil.python()) + // .unwrap(); + // } + + fn y_d(&self, i: usize, d: u128) -> u128 { + let mut xx = self.xp(); + xx.remove(i); + let s: u128 = xx.iter().sum(); + let ann = self.amp_factor * self.n_coins as u128; + let mut c = d; + for y in &xx { + c = c * d / (y * self.n_coins as u128); + } + c = c * d / (self.n_coins as u128 * ann); + let b = s + d / ann; + let mut y_prev = 0; + let mut y = d; + + let mut counter = 0; + + while y.abs_diff(y_prev) > 1 { + y_prev = y; + y = (y.pow(2) + c) / (2 * y + b - d); + + counter += 1; + if counter > 1000 { + break; + } + } + y + } + + pub fn sim_y_d(&self, i: u128, d: u128) -> u128 { + self.y_d(i as usize, d) + } + + // pub fn sim_y_d(&self, i: u128, d: u128) -> u128 { + // let gil = Python::acquire_gil(); + // return self + // .call1(gil.python(), "y_D", (i, d)) + // .unwrap() + // .extract(gil.python()) + // .unwrap(); + // } + + fn remove_liquidity_imbalance(&mut self, amounts: Vec) -> u128 { + let _fee = self.fee * self.n_coins as u128 / (4 * (self.n_coins as u128 - 1)); + + let old_balances = self.balances.clone(); + let mut new_balances = self.balances.clone(); + let d0 = self.d(); + for i in 0..self.n_coins as usize { + new_balances[i] -= amounts[i]; + } + self.balances = new_balances.clone(); + let d1 = self.d(); + self.balances = old_balances.clone(); + let mut fees = vec![0; self.n_coins as usize]; + for i in 0..self.n_coins as usize { + let ideal_balance = d1 * old_balances[i] / d0; + let difference = ideal_balance.abs_diff(new_balances[i]); + fees[i] = _fee * difference / 10u128.pow(10); + new_balances[i] -= fees[i]; + } + self.balances = new_balances; + let d2 = self.d(); + self.balances = old_balances; + + (d0 - d2) * self.pool_tokens / d0 + } + + pub fn sim_remove_liquidity_imbalance(&mut self, amounts: Vec) -> u128 { + self.remove_liquidity_imbalance(amounts) + } + + // pub fn sim_remove_liquidity_imbalance(&self, amounts: Vec) -> u128 { + // let gil = Python::acquire_gil(); + // return self + // .call1( + // gil.python(), + // "remove_liquidity_imbalance", + // PyTuple::new(gil.python(), amounts.to_vec()), + // ) + // .unwrap() + // .extract(gil.python()) + // .unwrap(); + // } + + fn calc_withdraw_one_coin(&self, token_amount: u128, i: usize) -> u128 { + let xp = self.xp(); + let fee = if self.fee > 0 { + self.fee - self.fee * xp[i] / xp.iter().sum::() + 5 * 10u128.pow(5) + } else { + 0 + }; + + let d0 = self.d(); + let d1 = d0 - token_amount * d0 / self.pool_tokens; + let dy = xp[i] - self.y_d(i, d1); + + dy - dy * fee / 10u128.pow(10) + } + + pub fn sim_calc_withdraw_one_coin(&self, token_amount: u128, i: u128) -> u128 { + self.calc_withdraw_one_coin(token_amount, i as usize) + } + + // fn call0(&self, py: Python, method_name: &str) -> Result { + // let sim = PyModule::from_code(py, &self.py_src, FILE_NAME, MODULE_NAME).unwrap(); + // let model = sim + // .call_method1("Curve", + // ( + // self.amp_factor, + // self.balances.to_vec(), + // self.n_coins, + // self.target_prices.to_vec(), + // self.pool_tokens, + // ) + // ) + // .unwrap() + // .to_object(py); + // let py_ret = model.as_ref(py).call_method0(method_name); + // self.extract_py_ret(py, py_ret) + // } + + // fn call1( + // &self, + // py: Python, + // method_name: &str, + // args: impl IntoPy>, + // ) -> Result { + // let sim = PyModule::from_code(py, &self.py_src, FILE_NAME, MODULE_NAME).unwrap(); + // let model = sim + // .call_method1("Curve",( + // self.amp_factor, + // self.balances.to_vec(), + // self.n_coins, + // self.target_prices.to_vec(), + // self.pool_tokens, + // )) + // .unwrap() + // .to_object(py); + // let py_ret = model.as_ref(py).call_method1(method_name, args); + // self.extract_py_ret(py, py_ret) + // } + + // fn extract_py_ret(&self, py: Python, ret: PyResult<&PyAny>) -> Result { + // match ret { + // Ok(v) => v.extract(), + // Err(e) => { + // e.print_and_set_sys_last_vars(py); + // panic!("Python execution failed.") + // } + // } + // } +} diff --git a/packages/circular_buffer/Cargo.toml b/packages/circular_buffer/Cargo.toml new file mode 100644 index 0000000..c2a3afb --- /dev/null +++ b/packages/circular_buffer/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "palomadex-circular-buffer" +version = "0.1.0" +edition = "2021" +authors = ["Volume Finanace"] +description = "Circular buffer to store time series data in CosmWasm contracts" +license = "Apache-2.0" + +[dependencies] +cw-storage-plus.workspace = true +cosmwasm-schema.workspace = true +cosmwasm-std.workspace = true +thiserror.workspace = true \ No newline at end of file diff --git a/packages/circular_buffer/src/error.rs b/packages/circular_buffer/src/error.rs new file mode 100644 index 0000000..28fc75a --- /dev/null +++ b/packages/circular_buffer/src/error.rs @@ -0,0 +1,59 @@ +use cosmwasm_std::StdError; +use thiserror::Error; + +pub type BufferResult = Result; + +/// This enum describes pair contract errors +#[derive(Error, Debug, PartialEq)] +pub enum BufferError { + #[error("{0}")] + Std(#[from] StdError), + + #[error("Can't reduce capacity because buffer contains key(s) greater than new head")] + ReduceCapacityError {}, + + #[error("Can't save value at index {0} which greater than capacity")] + SaveValueError(u32), + + #[error("Can't read value at index {0} which greater than capacity")] + ReadAheadError(u32), + + #[error("Index {0} not found")] + IndexNotFound(u32), + + #[error("Buffer not initialized")] + BufferNotInitialized {}, + + #[error("Buffer already initialized")] + BufferAlreadyInitialized {}, +} + +impl From for StdError { + fn from(value: BufferError) -> Self { + match value { + BufferError::Std(err) => err, + _ => StdError::generic_err(value.to_string()), + } + } +} + +#[cfg(test)] +mod testing { + use super::*; + + #[test] + fn test_buffer_error() { + let err = BufferError::Std(StdError::generic_err("test")); + let std_err: StdError = err.into(); + assert_eq!(std_err, StdError::generic_err("test")); + + let custom_err = BufferError::ReduceCapacityError {}; + let std_err: StdError = custom_err.into(); + assert_eq!( + std_err, + StdError::generic_err( + "Can't reduce capacity because buffer contains key(s) greater than new head" + ) + ); + } +} diff --git a/packages/circular_buffer/src/lib.rs b/packages/circular_buffer/src/lib.rs new file mode 100644 index 0000000..5a07352 --- /dev/null +++ b/packages/circular_buffer/src/lib.rs @@ -0,0 +1,353 @@ +//! Circular buffer which is built over [`Item`] and [`Map`]. +//! Might be useful to store time series data in contracts. +//! +//! # Example +//! ``` +//! use cosmwasm_std::testing::MockStorage; +//! use palomadex_circular_buffer::{BufferManager, CircularBuffer}; +//! +//! const CIRCULAR_BUFFER: CircularBuffer = CircularBuffer::new("buffer_state", "buffer"); +//! +//! let mut store = MockStorage::new(); +//! BufferManager::init(&mut store, CIRCULAR_BUFFER, 10).unwrap(); +//! let mut buffer = BufferManager::new(&store, CIRCULAR_BUFFER).unwrap(); +//! +//! let data = (1..=10u128).collect::>(); +//! buffer.push_many(&data); +//! buffer.commit(&mut store).unwrap(); +//! +//! let values = buffer.read(&store, 0u32..=9, true).unwrap(); +//! let all_values = buffer.read_all(&store).unwrap(); +//! ``` + +use std::collections::HashMap; +use std::fmt::{Debug, Display, Formatter}; +use std::marker::PhantomData; + +use cosmwasm_schema::cw_serde; +use cosmwasm_schema::serde::de::DeserializeOwned; +use cosmwasm_schema::serde::Serialize; +use cosmwasm_std::{StdError, Storage}; +use cw_storage_plus::{Item, Map}; + +use crate::error::{BufferError, BufferResult}; + +pub mod error; + +#[cw_serde] +pub struct BufferState { + capacity: u32, + head: u32, +} + +pub struct CircularBuffer<'a, V> { + state_key: &'a str, + array_namespace: &'a str, + data_type: PhantomData, +} + +impl<'a, V> CircularBuffer<'a, V> { + pub const fn new(state_key: &'a str, array_namespace: &'a str) -> Self { + Self { + state_key, + array_namespace, + data_type: PhantomData, + } + } + + pub const fn state(&'a self) -> Item { + Item::new(self.state_key) + } + + pub const fn array(&'a self) -> Map { + Map::new(self.array_namespace) + } +} + +pub struct BufferManager<'a, V> { + state: BufferState, + store_iface: CircularBuffer<'a, V>, + precommit_buffer: HashMap, +} + +impl<'a, V> BufferManager<'a, V> +where + V: Serialize + DeserializeOwned + 'a, +{ + /// Static function to initialize buffer in storage. + /// Intended to be called during contract initialization. + pub fn init( + store: &mut dyn Storage, + store_iface: CircularBuffer<'a, V>, + capacity: u32, + ) -> BufferResult<()> { + let state_iface = store_iface.state(); + + if state_iface.may_load(store)?.is_some() { + return Err(BufferError::BufferAlreadyInitialized {}); + } + + state_iface.save(store, &BufferState { capacity, head: 0 })?; + + Ok(()) + } + + /// Initialize buffer manager. + /// In case buffer is not initialized it throws [`BufferError::BufferNotInitialized`] error. + pub fn new(store: &dyn Storage, store_iface: CircularBuffer<'a, V>) -> BufferResult { + Ok(Self { + state: store_iface.state().load(store).map_err(|err| { + if let StdError::NotFound { .. } = err { + BufferError::BufferNotInitialized {} + } else { + err.into() + } + })?, + store_iface, + precommit_buffer: HashMap::new(), + }) + } + + /// Returns current buffer capacity. + pub fn capacity(&self) -> u32 { + self.state.capacity + } + + /// Returns current buffer head. + pub fn head(&self) -> u32 { + self.state.head + } + + /// Push value to precommit buffer. + pub fn push(&mut self, value: &'a V) { + self.precommit_buffer.insert(self.state.head, value); + self.state.head = (self.state.head + 1) % self.state.capacity; + } + + /// Push multiple values to precommit buffer. + pub fn push_many(&mut self, values: &'a [V]) { + for value in values { + self.push(value); + } + } + + /// Push value to precommit buffer and commit it to storage. + pub fn instant_push(&mut self, store: &mut dyn Storage, value: &'a V) -> BufferResult<()> { + self.push(value); + self.commit(store) + } + + /// Commit in storage current state and precommit buffer. Buffer is erased after commit. + pub fn commit(&mut self, store: &mut dyn Storage) -> BufferResult<()> { + let array_key = self.store_iface.array(); + for (&key, value) in &self.precommit_buffer { + if key >= self.state.capacity { + return Err(BufferError::SaveValueError(key)); + } + array_key.save(store, key, value)?; + } + self.precommit_buffer.clear(); + self.store_iface.state().save(store, &self.state)?; + + Ok(()) + } + + /// Read values from storage by indexes. If `stop_if_empty` is true, + /// reading will stop when first empty value is encountered. + /// Otherwise, [`BufferError::IndexNotFound`] error will be thrown. + /// + /// ## Examples: + /// ``` + /// # use cosmwasm_std::{testing::MockStorage}; + /// # use palomadex_circular_buffer::{BufferManager, CircularBuffer}; + /// # let mut store = MockStorage::new(); + /// # const CIRCULAR_BUFFER: CircularBuffer = CircularBuffer::new("buffer_state", "buffer"); + /// # BufferManager::init(&mut store, CIRCULAR_BUFFER, 10).unwrap(); + /// # let mut buffer = BufferManager::new(&store, CIRCULAR_BUFFER).unwrap(); + /// # let data = (1..=10u128).collect::>(); + /// # buffer.push_many(&data); + /// # buffer.commit(&mut store).unwrap(); + /// + /// let values = buffer.read(&store, 0u32..=9, false).unwrap(); + /// let values = buffer.read(&store, vec![0u32, 5, 7], false).unwrap(); + /// let values = buffer.read(&store, (0u32..buffer.capacity()).step_by(2), false).unwrap(); + /// ``` + pub fn read( + &self, + store: &dyn Storage, + indexes: impl IntoIterator + Display>, + stop_if_empty: bool, + ) -> BufferResult> { + let array_key = self.store_iface.array(); + let mut values = vec![]; + for index in indexes { + let ind = index.into(); + if ind > self.state.capacity - 1 { + return Err(BufferError::ReadAheadError(ind)); + } else { + let value = array_key.load(store, ind).map_err(|err| { + if let StdError::NotFound { .. } = err { + BufferError::IndexNotFound(ind) + } else { + err.into() + } + }); + match value { + Ok(value) => values.push(value), + Err(BufferError::IndexNotFound(_)) if stop_if_empty => return Ok(values), + Err(err) => return Err(err), + } + } + } + + Ok(values) + } + + /// Read all available values from storage. + pub fn read_all(&self, store: &dyn Storage) -> BufferResult> { + self.read(store, 0..self.state.capacity, true) + } + + /// Read last saved value from storage. Returns None if buffer is empty. + pub fn read_last(&self, store: &dyn Storage) -> BufferResult> { + self.read_single( + store, + (self.state.capacity + self.state.head - 1) % self.state.capacity, + ) + } + + /// Looped read. Returns None if value in buffer does not exist. + pub fn read_single( + &self, + store: &dyn Storage, + index: impl Into, + ) -> BufferResult> { + let ind = index.into() % self.state.capacity; + let res = self.store_iface.array().load(store, ind); + if let Err(StdError::NotFound { .. }) = res { + Ok(None) + } else { + res.map(Some).map_err(Into::into) + } + } + + /// This operation is gas consuming. However, it might be helpful in rare cases. + pub fn clear_buffer(&self, store: &mut dyn Storage) { + let array_key = self.store_iface.array(); + (0..self.state.capacity).for_each(|i| array_key.remove(store, i)) + } + + /// Whether index exists in buffer. + pub fn exists(&self, store: &dyn Storage, index: u32) -> bool { + self.store_iface + .array() + .has(store, index % self.state.capacity) + } +} + +impl Debug for BufferManager<'_, V> { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_struct("BufferManager") + .field("state", &self.state) + .field("precommit_buffer", &self.precommit_buffer) + .finish() + } +} + +#[cfg(test)] +mod tests { + use cosmwasm_std::testing::MockStorage; + use cosmwasm_std::Uint128; + + use super::*; + + type DataType = Uint128; + const CIRCULAR_BUFFER: CircularBuffer = CircularBuffer::new("buffer_state", "buffer"); + + #[test] + fn test_single_push() { + let mut store = MockStorage::new(); + + BufferManager::init(&mut store, CIRCULAR_BUFFER, 10).unwrap(); + + // Buffer can be initialized only once + let err = BufferManager::init(&mut store, CIRCULAR_BUFFER, 10).unwrap_err(); + assert_eq!(err, BufferError::BufferAlreadyInitialized {}); + + let mut buffer = BufferManager::new(&store, CIRCULAR_BUFFER).unwrap(); + + assert_eq!(buffer.capacity(), 10); + assert_eq!( + format!("{:?}", &buffer), + "BufferManager { state: BufferState { capacity: 10, head: 0 }, precommit_buffer: {} }" + ); + + let data = (1..=15u8).map(DataType::from).collect::>(); + data.iter().for_each(|i| buffer.push(i)); + buffer.commit(&mut store).unwrap(); + + // read last saved value + let head = buffer.read_last(&store).unwrap().unwrap(); + assert_eq!(head.u128(), 15); + + let saved = buffer + .read(&store, 0u32..=9, true) + .unwrap() + .into_iter() + .map(|i| i.u128()) + .collect::>(); + assert_eq!(saved, vec![11, 12, 13, 14, 15, 6, 7, 8, 9, 10]); + + // check instant push + let val = DataType::from(16u128); + buffer.instant_push(&mut store, &val).unwrap(); + let saved = buffer + .read(&store, 0u32..=9, true) + .unwrap() + .into_iter() + .map(|i| i.u128()) + .collect::>(); + assert_eq!(saved, vec![11, 12, 13, 14, 15, 16, 7, 8, 9, 10]); + + // read invalid index + let err = buffer.read(&store, [10u32, 11u32], true).unwrap_err(); + assert_eq!(err, BufferError::ReadAheadError(10)); + } + + #[test] + fn test_push_many() { + let mut store = MockStorage::new(); + + // Trying to create buffer manager before initialization + let err = BufferManager::new(&store, CIRCULAR_BUFFER).unwrap_err(); + assert_eq!(err, BufferError::BufferNotInitialized {}); + + BufferManager::init(&mut store, CIRCULAR_BUFFER, 10).unwrap(); + + let mut buffer = BufferManager::new(&store, CIRCULAR_BUFFER).unwrap(); + + // read empty buffer + let err = buffer.read(&store, [0u8], false).unwrap_err(); + assert_eq!(err, BufferError::IndexNotFound(0)); + + let data = (1..=15u8).map(DataType::from).collect::>(); + buffer.push_many(&data); + buffer.commit(&mut store).unwrap(); + + let saved = buffer + .read_all(&store) + .unwrap() + .into_iter() + .map(|i| i.u128()) + .collect::>(); + assert_eq!(saved, vec![11, 12, 13, 14, 15, 6, 7, 8, 9, 10]); + + let partial_read = buffer + .read(&store, (0u32..buffer.capacity()).step_by(2), true) + .unwrap() + .into_iter() + .map(|i| i.u128()) + .collect::>(); + assert_eq!(partial_read, vec![11, 13, 15, 7, 9]); + } +} diff --git a/packages/native_coin_registry/.cargo/config.toml b/packages/native_coin_registry/.cargo/config.toml new file mode 100644 index 0000000..74120c2 --- /dev/null +++ b/packages/native_coin_registry/.cargo/config.toml @@ -0,0 +1,4 @@ +[alias] +wasm = "build --release --lib --target wasm32-unknown-unknown" +unit-test = "test --lib" +schema = "run --bin native_coin_registry_schema" diff --git a/packages/native_coin_registry/Cargo.toml b/packages/native_coin_registry/Cargo.toml new file mode 100644 index 0000000..d1e3ee6 --- /dev/null +++ b/packages/native_coin_registry/Cargo.toml @@ -0,0 +1,33 @@ +[package] +name = "palomadex-native-coin-registry" +version = "0.1.0" +authors = ["Volume Finance"] +edition = "2021" + +exclude = [ + # Those files are rust-optimizer artifacts. You might want to commit them for convenience but they should not be part of the source code publication. + "contract.wasm", + "hash.txt", +] + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +# use library feature to disable all instantiate/execute/query exports +library = [] + +[lints.rust] +unexpected_cfgs = { level = "warn", check-cfg = ["cfg(tarpaulin_include)"] } + +[dependencies] +cosmwasm-schema.workspace = true +cosmwasm-std.workspace = true +cw-storage-plus.workspace = true +thiserror.workspace = true +palomadex.workspace = true + +[dev-dependencies] +cw-multi-test = "1.2.0" diff --git a/packages/native_coin_registry/README.md b/packages/native_coin_registry/README.md new file mode 100644 index 0000000..e7e46f2 --- /dev/null +++ b/packages/native_coin_registry/README.md @@ -0,0 +1,3 @@ +# Palomadex native coins registry contract + +The registry contract contains native assets with their precision. \ No newline at end of file diff --git a/packages/native_coin_registry/src/contract.rs b/packages/native_coin_registry/src/contract.rs new file mode 100644 index 0000000..a5eee48 --- /dev/null +++ b/packages/native_coin_registry/src/contract.rs @@ -0,0 +1,180 @@ +#[cfg(not(feature = "library"))] +use cosmwasm_std::entry_point; +use cosmwasm_std::{ + attr, to_json_binary, Binary, Deps, DepsMut, Env, MessageInfo, Order, Response, StdError, + StdResult, +}; +use cw_storage_plus::Bound; +use std::collections::HashSet; + +use crate::error::ContractError; +use crate::state::{CONFIG, OWNERSHIP_PROPOSAL}; +use palomadex::common::{claim_ownership, drop_ownership_proposal, propose_new_owner}; +use palomadex::native_coin_registry::{ + CoinResponse, Config, ExecuteMsg, InstantiateMsg, QueryMsg, COINS_INFO, +}; + +/// Settings for pagination. +const MAX_LIMIT: u32 = 30; +const DEFAULT_LIMIT: u32 = 10; + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn instantiate( + deps: DepsMut, + _env: Env, + _info: MessageInfo, + msg: InstantiateMsg, +) -> Result { + CONFIG.save( + deps.storage, + &Config { + owner: deps.api.addr_validate(msg.owner.as_str())?, + }, + )?; + + Ok(Response::default()) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn execute( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: ExecuteMsg, +) -> Result { + match msg { + ExecuteMsg::Add { native_coins } => update(deps, info, native_coins), + ExecuteMsg::Remove { native_coins } => remove(deps, info, native_coins), + ExecuteMsg::ProposeNewOwner { owner, expires_in } => { + let config = CONFIG.load(deps.storage)?; + + propose_new_owner( + deps, + info, + env, + owner, + expires_in, + config.owner, + OWNERSHIP_PROPOSAL, + ) + .map_err(Into::into) + } + ExecuteMsg::DropOwnershipProposal {} => { + let config = CONFIG.load(deps.storage)?; + + drop_ownership_proposal(deps, info, config.owner, OWNERSHIP_PROPOSAL) + .map_err(Into::into) + } + ExecuteMsg::ClaimOwnership {} => { + claim_ownership(deps, info, env, OWNERSHIP_PROPOSAL, |deps, new_owner| { + CONFIG + .update::<_, StdError>(deps.storage, |mut v| { + v.owner = new_owner; + Ok(v) + }) + .map(|_| ()) + }) + .map_err(Into::into) + } + } +} + +/// Adds or updates a native asset in the registry. +/// +/// * **native_coins** is a vector with the assets we are adding to the registry. +/// +/// ## Executor +/// Only the owner can execute this. +pub fn update( + deps: DepsMut, + info: MessageInfo, + native_coins: Vec<(String, u8)>, +) -> Result { + let config = CONFIG.load(deps.storage)?; + + if info.sender != config.owner { + return Err(ContractError::Unauthorized {}); + } + + // Check for duplicate native coins + let mut uniq = HashSet::new(); + if !native_coins.iter().all(|a| uniq.insert(&a.0)) { + return Err(ContractError::DuplicateCoins {}); + } + + for (coin, decimals) in native_coins { + if decimals == 0 { + return Err(ContractError::CoinWithZeroPrecision(coin)); + } + + COINS_INFO.save(deps.storage, coin, &decimals)?; + } + + Ok(Response::new().add_attributes(vec![attr("action", "add")])) +} + +/// Removes an existing native asset from the registry. +/// +/// * **native_coins** is a vector with the assets we are removing from the contract. +/// +/// ## Executor +/// Only the owner can execute this. +pub fn remove( + deps: DepsMut, + info: MessageInfo, + native_coins: Vec, +) -> Result { + let config = CONFIG.load(deps.storage)?; + + if info.sender != config.owner { + return Err(ContractError::Unauthorized {}); + } + + // Check for duplicate native coins + let mut uniq = HashSet::new(); + if !native_coins.iter().all(|a| uniq.insert(a)) { + return Err(ContractError::DuplicateCoins {}); + } + + for coin in native_coins { + if COINS_INFO.has(deps.storage, coin.clone()) { + COINS_INFO.remove(deps.storage, coin); + } else { + return Err(ContractError::CoinDoesNotExist(coin)); + } + } + + Ok(Response::new().add_attributes(vec![attr("action", "remove")])) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { + match msg { + QueryMsg::Config {} => Ok(to_json_binary(&CONFIG.load(deps.storage)?)?), + QueryMsg::NativeToken { denom } => { + Ok(to_json_binary(&COINS_INFO.load(deps.storage, denom)?)?) + } + QueryMsg::NativeTokens { start_after, limit } => { + to_json_binary(&query_native_tokens(deps, start_after, limit)?) + } + } +} + +/// Returns a vector with native assets by specified parameters. +pub fn query_native_tokens( + deps: Deps, + start_after: Option, + limit: Option, +) -> StdResult> { + let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize; + let start = start_after.map(Bound::exclusive); + + COINS_INFO + .range(deps.storage, start, None, Order::Ascending) + .map(|pair| { + let (denom, decimals) = pair?; + Ok(CoinResponse { denom, decimals }) + }) + .take(limit) + .collect::>>() +} diff --git a/packages/native_coin_registry/src/error.rs b/packages/native_coin_registry/src/error.rs new file mode 100644 index 0000000..383c95c --- /dev/null +++ b/packages/native_coin_registry/src/error.rs @@ -0,0 +1,23 @@ +use cosmwasm_std::StdError; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum ContractError { + #[error("{0}")] + Std(#[from] StdError), + + #[error("Unauthorized")] + Unauthorized {}, + + #[error("Contract can't be migrated!")] + MigrationError {}, + + #[error("Duplicate coins are provided")] + DuplicateCoins {}, + + #[error("The coin cannot have zero precision: {0}")] + CoinWithZeroPrecision(String), + + #[error("The coin does not exist: {0}")] + CoinDoesNotExist(String), +} diff --git a/packages/native_coin_registry/src/lib.rs b/packages/native_coin_registry/src/lib.rs new file mode 100644 index 0000000..6c1df89 --- /dev/null +++ b/packages/native_coin_registry/src/lib.rs @@ -0,0 +1,5 @@ +pub mod contract; +mod error; +pub mod state; + +pub use crate::error::ContractError; diff --git a/packages/native_coin_registry/src/state.rs b/packages/native_coin_registry/src/state.rs new file mode 100644 index 0000000..b659a20 --- /dev/null +++ b/packages/native_coin_registry/src/state.rs @@ -0,0 +1,9 @@ +use cw_storage_plus::Item; +use palomadex::common::OwnershipProposal; +use palomadex::native_coin_registry::Config; + +/// Stores the contract config at the given key +pub const CONFIG: Item = Item::new("config"); + +/// Contains a proposal to change contract ownership. +pub const OWNERSHIP_PROPOSAL: Item = Item::new("ownership_proposal"); diff --git a/packages/native_coin_registry/tests/integration.rs b/packages/native_coin_registry/tests/integration.rs new file mode 100644 index 0000000..266d14e --- /dev/null +++ b/packages/native_coin_registry/tests/integration.rs @@ -0,0 +1,383 @@ +#![cfg(not(tarpaulin_include))] + +use cosmwasm_std::Addr; + +use cw_multi_test::{App, ContractWrapper, Executor}; +use palomadex::native_coin_registry::{CoinResponse, Config, ExecuteMsg, InstantiateMsg, QueryMsg}; + +fn mock_app() -> App { + App::default() +} + +fn store_native_registry_code(app: &mut App) -> u64 { + let contract = Box::new(ContractWrapper::new_with_empty( + palomadex_native_coin_registry::contract::execute, + palomadex_native_coin_registry::contract::instantiate, + palomadex_native_coin_registry::contract::query, + )); + + app.store_code(contract) +} + +#[test] +fn proper_initialization() { + let mut app = mock_app(); + let owner = Addr::unchecked("owner"); + + let native_registry_code_id = store_native_registry_code(&mut app); + let msg = InstantiateMsg { + owner: owner.to_string(), + }; + + let native_registry_instance = app + .instantiate_contract( + native_registry_code_id, + Addr::unchecked(owner.clone()), + &msg, + &[], + "Precision registry contract", + None, + ) + .unwrap(); + + let msg = QueryMsg::Config {}; + let config_res: Config = app + .wrap() + .query_wasm_smart(&native_registry_instance, &msg) + .unwrap(); + + assert_eq!(owner, config_res.owner); +} + +#[test] +fn check_update_owner() { + let mut app = mock_app(); + let owner = Addr::unchecked("owner"); + let new_owner = String::from("new_owner"); + + let native_registry_code_id = store_native_registry_code(&mut app); + let msg = InstantiateMsg { + owner: owner.to_string(), + }; + + let native_registry_instance = app + .instantiate_contract( + native_registry_code_id, + Addr::unchecked(owner.clone()), + &msg, + &[], + "Precision registry contract", + None, + ) + .unwrap(); + + // New owner + let msg = ExecuteMsg::ProposeNewOwner { + owner: new_owner.clone(), + expires_in: 100, // seconds + }; + + let err = app + .execute_contract( + Addr::unchecked("not_owner"), + native_registry_instance.clone(), + &msg, + &[], + ) + .unwrap_err(); + assert_eq!(err.root_cause().to_string(), "Generic error: Unauthorized"); + + // Claim before proposal + let err = app + .execute_contract( + Addr::unchecked(new_owner.clone()), + native_registry_instance.clone(), + &ExecuteMsg::ClaimOwnership {}, + &[], + ) + .unwrap_err(); + assert_eq!( + err.root_cause().to_string(), + "Generic error: Ownership proposal not found" + ); + + // Propose new owner + app.execute_contract( + Addr::unchecked("owner"), + native_registry_instance.clone(), + &msg, + &[], + ) + .unwrap(); + + // Claim from invalid addr + let err = app + .execute_contract( + Addr::unchecked("invalid_addr"), + native_registry_instance.clone(), + &ExecuteMsg::ClaimOwnership {}, + &[], + ) + .unwrap_err(); + assert_eq!(err.root_cause().to_string(), "Generic error: Unauthorized"); + + // Drop ownership proposal + let err = app + .execute_contract( + Addr::unchecked(new_owner.clone()), + native_registry_instance.clone(), + &ExecuteMsg::DropOwnershipProposal {}, + &[], + ) + .unwrap_err(); + // new_owner is not an owner yet + assert_eq!(err.root_cause().to_string(), "Generic error: Unauthorized"); + + app.execute_contract( + owner.clone(), + native_registry_instance.clone(), + &ExecuteMsg::DropOwnershipProposal {}, + &[], + ) + .unwrap(); + + // Try to claim ownership + let err = app + .execute_contract( + Addr::unchecked(new_owner.clone()), + native_registry_instance.clone(), + &ExecuteMsg::ClaimOwnership {}, + &[], + ) + .unwrap_err(); + assert_eq!( + err.root_cause().to_string(), + "Generic error: Ownership proposal not found" + ); + + // Propose new owner again + app.execute_contract( + Addr::unchecked("owner"), + native_registry_instance.clone(), + &msg, + &[], + ) + .unwrap(); + // Claim ownership + app.execute_contract( + Addr::unchecked(new_owner.clone()), + native_registry_instance.clone(), + &ExecuteMsg::ClaimOwnership {}, + &[], + ) + .unwrap(); + + // Let's query the contract state + let msg = QueryMsg::Config {}; + let res: Config = app + .wrap() + .query_wasm_smart(&native_registry_instance, &msg) + .unwrap(); + + assert_eq!(res.owner, new_owner) +} + +#[test] +fn try_add_and_remove_native_tokens() { + let mut app = mock_app(); + let owner = Addr::unchecked("owner"); + + let native_registry_code_id = store_native_registry_code(&mut app); + let msg = InstantiateMsg { + owner: owner.to_string(), + }; + + let native_registry_instance = app + .instantiate_contract( + native_registry_code_id, + Addr::unchecked(owner.clone()), + &msg, + &[], + "Precision registry contract", + None, + ) + .unwrap(); + + let msg = ExecuteMsg::Add { + native_coins: vec![ + ("ULUNA".to_string(), 18), + ("USDT".to_string(), 10), + ("usdc".to_string(), 0), + ("usdc".to_string(), 1), + ], + }; + + let err = app + .execute_contract( + Addr::unchecked("not_owner"), + native_registry_instance.clone(), + &msg, + &[], + ) + .unwrap_err(); + assert_eq!(err.root_cause().to_string(), "Unauthorized"); + + let err = app + .execute_contract( + Addr::unchecked("owner"), + native_registry_instance.clone(), + &msg, + &[], + ) + .unwrap_err(); + assert_eq!(err.root_cause().to_string(), "Duplicate coins are provided"); + + let msg = ExecuteMsg::Add { + native_coins: vec![ + ("ULUNA".to_string(), 18), + ("USDT".to_string(), 10), + ("usdc".to_string(), 0), + ], + }; + + let err = app + .execute_contract( + Addr::unchecked("owner"), + native_registry_instance.clone(), + &msg, + &[], + ) + .unwrap_err(); + + assert_eq!( + err.root_cause().to_string(), + "The coin cannot have zero precision: usdc" + ); + + let msg = ExecuteMsg::Add { + native_coins: vec![ + ("ULUNA".to_string(), 18), + ("USDT".to_string(), 10), + ("usdc".to_string(), 3), + ], + }; + + app.execute_contract( + Addr::unchecked("owner"), + native_registry_instance.clone(), + &msg, + &[], + ) + .unwrap(); + + // query asset info by denominator name + let coin_decimals: u8 = app + .wrap() + .query_wasm_smart( + &native_registry_instance, + &QueryMsg::NativeToken { + denom: "usdc".to_string(), + }, + ) + .unwrap(); + + assert_eq!(3, coin_decimals); + + // query asset info by denominator name + let config_res: Vec = app + .wrap() + .query_wasm_smart( + &native_registry_instance, + &QueryMsg::NativeTokens { + start_after: None, + limit: None, + }, + ) + .unwrap(); + + assert_eq!( + vec![ + CoinResponse { + denom: "ULUNA".to_string(), + decimals: 18 + }, + CoinResponse { + denom: "USDT".to_string(), + decimals: 10 + }, + CoinResponse { + denom: "usdc".to_string(), + decimals: 3 + } + ], + config_res + ); + + // query asset info by denominator name + let config_res: Vec = app + .wrap() + .query_wasm_smart( + &native_registry_instance, + &QueryMsg::NativeTokens { + start_after: Some("USDT".to_string()), + limit: None, + }, + ) + .unwrap(); + + assert_eq!( + vec![CoinResponse { + denom: "usdc".to_string(), + decimals: 3 + }], + config_res + ); + + let msg = ExecuteMsg::Remove { + native_coins: vec!["usdc".to_string()], + }; + + let err = app + .execute_contract( + Addr::unchecked("not_owner"), + native_registry_instance.clone(), + &msg, + &[], + ) + .unwrap_err(); + assert_eq!(err.root_cause().to_string(), "Unauthorized"); + + app.execute_contract( + Addr::unchecked("owner"), + native_registry_instance.clone(), + &msg, + &[], + ) + .unwrap(); + + // query asset info by denominator name + let config_res: Vec = app + .wrap() + .query_wasm_smart( + &native_registry_instance, + &QueryMsg::NativeTokens { + start_after: None, + limit: None, + }, + ) + .unwrap(); + + assert_eq!( + vec![ + CoinResponse { + denom: "ULUNA".to_string(), + decimals: 18 + }, + CoinResponse { + denom: "USDT".to_string(), + decimals: 10 + } + ], + config_res + ); +} diff --git a/packages/palomadex/Cargo.toml b/packages/palomadex/Cargo.toml index 7db34df..c4c2a2f 100644 --- a/packages/palomadex/Cargo.toml +++ b/packages/palomadex/Cargo.toml @@ -14,16 +14,16 @@ library = [] cosmwasm-schema.workspace = true cosmwasm-std = { workspace = true, features = ["cosmwasm_1_1"] } cw-storage-plus.workspace = true -cw2.workspace = true cw20.workspace = true cw-utils.workspace = true itertools.workspace = true schemars.workspace = true serde.workspace = true thiserror.workspace = true -cw-asset = "4.0.0" +cw-asset = "3.1.1" uint = "0.10.0" +palomadex-circular-buffer = { path = "../circular_buffer" } [dev-dependencies] -cw-multi-test = "2.0.0" +cw-multi-test = "1.2.0" test-case = "3.1.0" diff --git a/packages/palomadex/src/asset.rs b/packages/palomadex/src/asset.rs index a50670f..3565440 100644 --- a/packages/palomadex/src/asset.rs +++ b/packages/palomadex/src/asset.rs @@ -2,7 +2,9 @@ use std::fmt; use cosmwasm_schema::cw_serde; use cosmwasm_std::{ - coin, coins, ensure, to_json_binary, wasm_execute, Addr, Api, BankMsg, Binary, Coin, ConversionOverflowError, CosmosMsg, CustomMsg, CustomQuery, Decimal256, Fraction, MessageInfo, QuerierWrapper, ReplyOn, StdError, StdResult, SubMsg, Uint128, Uint256, WasmMsg + coin, coins, ensure, to_json_binary, wasm_execute, Addr, Api, BankMsg, Coin, + ConversionOverflowError, CosmosMsg, CustomMsg, CustomQuery, Decimal256, Fraction, MessageInfo, + QuerierWrapper, ReplyOn, StdError, StdResult, SubMsg, Uint128, Uint256, WasmMsg, }; use cw20::{Cw20Coin, Cw20CoinVerified, Cw20ExecuteMsg, Cw20QueryMsg, Denom, MinterResponse}; use cw_asset::{Asset as CwAsset, AssetInfo as CwAssetInfo}; @@ -43,7 +45,7 @@ pub struct DecimalAsset { } impl DecimalAsset { - pub fn into_asset(self, precision: impl Into + Sized) -> StdResult { + pub fn into_asset(self, precision: impl Into) -> StdResult { Ok(Asset { info: self.info, amount: self.amount.to_uint(precision)?, @@ -229,7 +231,6 @@ impl Asset { msg: inner_msg.into(), gas_limit: None, reply_on, - payload: Binary::default(), }) } AssetInfo::NativeToken { denom } => { @@ -244,7 +245,6 @@ impl Asset { msg: bank_msg, gas_limit: None, reply_on, - payload: Binary::default(), }) } } @@ -354,7 +354,7 @@ impl CoinsExt for Vec { /// ## Examples /// ``` /// # use cosmwasm_std::Addr; -/// # use astroport::asset::AssetInfo::{NativeToken, Token}; +/// # use palomadex::asset::AssetInfo::{NativeToken, Token}; /// Token { contract_addr: Addr::unchecked("stake...") }; /// NativeToken { denom: String::from("uluna") }; /// ``` @@ -390,8 +390,6 @@ impl<'a> Prefixer<'a> for &AssetInfo { impl KeyDeserialize for &AssetInfo { type Output = AssetInfo; - const KEY_ELEMS: u16 = 1; - #[inline(always)] fn from_vec(_value: Vec) -> StdResult { unimplemented!("Due to lack of knowledge of enum variant in binary there is no way to determine correct AssetInfo") @@ -598,7 +596,7 @@ pub fn validate_native_denom(denom: &str) -> StdResult<()> { Ok(()) } -/// This structure stores the main parameters for an Astroport pair +/// This structure stores the main parameters for an palomadex pair #[cw_serde] pub struct PairInfo { /// Asset information for the assets in the pool @@ -733,9 +731,9 @@ pub fn token_asset_info(contract_addr: Addr) -> AssetInfo { /// /// **NOTE** /// - this function relies on the fact that chain doesn't allow to mint native tokens in the form of bech32 addresses. -/// For example, if it is allowed to mint native token `wasm1xxxxxxx` then [`AssetInfo`] will be determined incorrectly; +/// For example, if it is allowed to mint native token `wasm1xxxxxxx` then [`AssetInfo`] will be determined incorrectly; /// - if you intend to test this functionality in cw-multi-test you must implement [`Api`] trait for your test App -/// with conjunction with [AddressGenerator](https://docs.rs/cw-multi-test/0.17.0/cw_multi_test/trait.AddressGenerator.html) +/// with conjunction with [AddressGenerator](https://docs.rs/cw-multi-test/0.17.0/cw_multi_test/trait.AddressGenerator.html) pub fn determine_asset_info(maybe_asset_info: &str, api: &dyn Api) -> StdResult { if api.addr_validate(maybe_asset_info).is_ok() { Ok(AssetInfo::Token { @@ -838,7 +836,10 @@ impl Decimal256Ext for Decimal256 { .checked_div(10u128.pow(self.decimal_places() - precision).into())? .try_into() .map_err(|o: ConversionOverflowError| { - StdError::generic_err(format!("Error converting from {} to {}", o.source_type, o.target_type)) + StdError::generic_err(format!( + "Error converting from {} to {}", + o.source_type, o.target_type + )) }) } diff --git a/packages/palomadex/src/factory.rs b/packages/palomadex/src/factory.rs index ec36127..ad86656 100644 --- a/packages/palomadex/src/factory.rs +++ b/packages/palomadex/src/factory.rs @@ -27,7 +27,7 @@ pub struct Config { /// This enum describes available pair types. /// ## Available pool types /// ``` -/// # use astroport::factory::PairType::{Custom, Stable, Xyk}; +/// # use palomadex::factory::PairType::{Custom, Stable, Xyk}; /// Xyk {}; /// Stable {}; /// Custom(String::from("Custom")); @@ -101,7 +101,7 @@ pub struct InstantiateMsg { pub generator_address: Option, /// Address of owner that is allowed to change factory contract parameters pub owner: String, - /// CW1 whitelist contract code id used to store 3rd party rewards for staking Astroport LP tokens + /// CW1 whitelist contract code id used to store 3rd party rewards for staking Palomadex LP tokens pub whitelist_code_id: u64, /// The address of the contract that contains the coins and their accuracy pub coin_registry_address: String, @@ -116,9 +116,9 @@ pub enum ExecuteMsg { token_code_id: Option, /// Contract address to send governance fees to (the Maker) fee_address: Option, - /// Contract address where Lp tokens can be auto_staked after someone provides liquidity in an incentivized Astroport pool + /// Contract address where Lp tokens can be auto_staked after someone provides liquidity in an incentivized Palomadex pool generator_address: Option, - /// CW1 whitelist contract code id used to store 3rd party rewards for staking Astroport LP tokens + /// CW1 whitelist contract code id used to store 3rd party rewards for staking Palomadex LP tokens whitelist_code_id: Option, /// The address of the contract that contains the coins and their accuracy coin_registry_address: Option, @@ -199,9 +199,9 @@ pub struct ConfigResponse { pub token_code_id: u64, /// Address of contract to send governance fees to (the Maker) pub fee_address: Option, - /// Address of contract used to auto_stake LP tokens for Astroport pairs that are incentivized + /// Address of contract used to auto_stake LP tokens for Palomadex pairs that are incentivized pub generator_address: Option, - /// CW1 whitelist contract code id used to store 3rd party rewards for staking Astroport LP tokens + /// CW1 whitelist contract code id used to store 3rd party rewards for staking Palomadex LP tokens pub whitelist_code_id: u64, /// The address of the contract that contains the coins and their accuracy pub coin_registry_address: Addr, diff --git a/packages/palomadex/src/lib.rs b/packages/palomadex/src/lib.rs index 5eed1ed..b040f5c 100644 --- a/packages/palomadex/src/lib.rs +++ b/packages/palomadex/src/lib.rs @@ -1,11 +1,14 @@ pub mod asset; +pub mod common; pub mod cosmwasm_ext; pub mod factory; +pub mod native_coin_registry; +pub mod observation; pub mod pair; pub mod querier; -pub mod native_coin_registry; -pub mod common; +pub mod router; pub mod token; +pub use decimal_checked_ops::DecimalCheckedOps; pub use uints::U256; #[allow(clippy::all)] @@ -15,4 +18,39 @@ mod uints { construct_uint! { pub struct U256(4); } -} \ No newline at end of file +} + +mod decimal_checked_ops { + use std::convert::TryInto; + + use cosmwasm_std::{Decimal, Fraction, OverflowError, Uint128, Uint256}; + + pub trait DecimalCheckedOps { + fn checked_add(self, other: Decimal) -> Result; + fn checked_mul_uint128(self, other: Uint128) -> Result; + } + + impl DecimalCheckedOps for Decimal { + fn checked_add(self, other: Decimal) -> Result { + self.numerator() + .checked_add(other.numerator()) + .map(|_| self + other) + } + fn checked_mul_uint128(self, other: Uint128) -> Result { + if self.is_zero() || other.is_zero() { + return Ok(Uint128::zero()); + } + let multiply_ratio = + other.full_mul(self.numerator()) / Uint256::from(self.denominator()); + if multiply_ratio > Uint256::from(Uint128::MAX) { + Err(OverflowError::new( + cosmwasm_std::OverflowOperation::Mul, + self, + other, + )) + } else { + Ok(multiply_ratio.try_into().unwrap()) + } + } + } +} diff --git a/packages/palomadex/src/observation.rs b/packages/palomadex/src/observation.rs new file mode 100644 index 0000000..319d754 --- /dev/null +++ b/packages/palomadex/src/observation.rs @@ -0,0 +1,260 @@ +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{ + CustomQuery, Decimal, Decimal256, Deps, Env, Fraction, StdError, StdResult, Storage, Uint128, + Uint256, +}; +use cw_storage_plus::Item; + +use palomadex_circular_buffer::{BufferManager, CircularBuffer}; + +use crate::cosmwasm_ext::AbsDiff; + +/// Circular buffer size which stores observations +pub const OBSERVATIONS_SIZE: u32 = 3000; + +/// Stores trade size observations. We use it in orderbook integration +/// and derive prices for external contracts/users. +#[cw_serde] +#[derive(Copy, Default)] +pub struct Observation { + /// Timestamp of the observation + pub ts: u64, + /// Observed price at this point + pub price: Decimal, + /// Price simple moving average (mean) + pub price_sma: Decimal, +} + +#[cw_serde] +pub struct OracleObservation { + pub timestamp: u64, + pub price: Decimal, +} + +/// Returns price observation at point that was 'seconds_ago' seconds ago. +pub fn query_observation( + deps: Deps, + env: Env, + observations: CircularBuffer, + seconds_ago: u64, +) -> StdResult +where + C: CustomQuery, +{ + let buffer = BufferManager::new(deps.storage, observations)?; + let target = env.block.time.seconds() - seconds_ago; + + let mut oldest_ind = buffer.head(); + let mut newest_ind = buffer.head() + buffer.capacity() - 1; + + if !buffer.exists(deps.storage, oldest_ind) { + if buffer.head() > 0 { + oldest_ind = 0; + newest_ind %= buffer.capacity(); + } else { + return match PrecommitObservation::may_load(deps.storage)? { + // First observation after pool initialization could be captured but not committed yet + Some(obs) if obs.precommit_ts <= target => Ok(OracleObservation { + timestamp: target, + price: Decimal::from_ratio(obs.base_amount, obs.quote_amount), + }), + Some(_) => Err(StdError::generic_err(format!( + "Requested observation is too old. Last known observation is at {}", + target + ))), + None => Err(StdError::generic_err("Buffer is empty")), + }; + } + } + + let newest_obs = buffer.read_single(deps.storage, newest_ind)?.unwrap(); + if target >= newest_obs.ts { + return Ok(OracleObservation { + timestamp: target, + price: newest_obs.price_sma, + }); + } + let oldest_obs = buffer.read_single(deps.storage, oldest_ind)?.unwrap(); + if target == oldest_obs.ts { + return Ok(OracleObservation { + timestamp: target, + price: oldest_obs.price_sma, + }); + } + if target < oldest_obs.ts { + return Err(StdError::generic_err(format!( + "Requested observation is too old. Last known observation is at {}", + oldest_obs.ts + ))); + } + + let (left, right) = binary_search(deps.storage, &buffer, target, oldest_ind, newest_ind)?; + + let price_left = left.price_sma; + let price_right = right.price_sma; + let price = if left.ts == target { + price_left + } else if right.ts == target { + price_right + } else if price_left == price_right { + price_left + } else { + // Interpolate. + let price_slope = + price_right.diff(price_left) * Decimal::from_ratio(1u8, right.ts - left.ts); + let time_interval = Decimal::from_ratio(target - left.ts, 1u8); + if price_left > price_right { + price_left - price_slope * time_interval + } else { + price_left + price_slope * time_interval + } + }; + + Ok(OracleObservation { + timestamp: target, + price, + }) +} + +/// Performs binary search in circular buffer. Returns left and right bounds of target value. +/// Either left or right bound may hit in target value. +fn binary_search( + storage: &dyn Storage, + buffer: &BufferManager, + target: u64, + mut start: u32, + mut end: u32, +) -> StdResult<(Observation, Observation)> { + loop { + let mid = (start + end) / 2; + + // We've checked bounds before calling this function thus these errors should be impossible. + let leftward_or_hit = buffer.read_single(storage, mid)?.ok_or_else(|| { + StdError::generic_err(format!( + "Unexpected error in binary_search: leftward_or_hit is None at index {mid}", + )) + })?; + let rightward_or_hit = buffer.read_single(storage, mid + 1)?.ok_or_else(|| { + StdError::generic_err(format!( + "Unexpected error in binary_search: rightward_or_hit is None at index {}", + mid + 1 + )) + })?; + + if leftward_or_hit.ts <= target && target <= rightward_or_hit.ts { + break Ok((leftward_or_hit, rightward_or_hit)); + } + if leftward_or_hit.ts > target { + end = mid - 1; + } else { + start = mid + 1; + } + } +} + +#[cw_serde] +pub struct PrecommitObservation { + pub base_amount: Uint128, + pub quote_amount: Uint128, + pub precommit_ts: u64, +} + +impl<'a> PrecommitObservation { + /// Temporal storage for observation which should be committed in the next block + const PRECOMMIT_OBSERVATION: Item<'a, PrecommitObservation> = + Item::new("precommit_observation"); + + pub fn save( + storage: &mut dyn Storage, + env: &Env, + base_amount: Uint128, + quote_amount: Uint128, + ) -> StdResult<()> { + let next_obs = match Self::may_load(storage)? { + // Accumulating observations at the same block + Some(mut prev_obs) if env.block.time.seconds() == prev_obs.precommit_ts => { + prev_obs.base_amount += base_amount; + prev_obs.quote_amount += quote_amount; + prev_obs + } + _ => PrecommitObservation { + base_amount, + quote_amount, + precommit_ts: env.block.time.seconds(), + }, + }; + + Self::PRECOMMIT_OBSERVATION.save(storage, &next_obs) + } + + #[inline] + pub fn may_load(storage: &dyn Storage) -> StdResult> { + Self::PRECOMMIT_OBSERVATION.may_load(storage) + } +} + +pub fn try_dec256_into_dec(val: Decimal256) -> StdResult { + let numerator: Uint128 = val.numerator().try_into()?; + + Ok(Decimal::from_ratio(numerator, Decimal::one().denominator())) +} + +/// Internal function to calculate new moving average using Uint256. +/// Overflow is possible only if new average price is greater than 2^128 - 1 which is unlikely. +/// Formula: (sma * count + new_price - oldest_price) / count +pub fn safe_sma_calculation( + price_sma: Decimal, + oldest_price: Decimal, + count: u32, + new_price: Decimal, +) -> StdResult { + let sma_times_count = price_sma.numerator().full_mul(count); + let res = Decimal256::from_ratio( + sma_times_count + Uint256::from(new_price.numerator()) + - Uint256::from(oldest_price.numerator()), + price_sma.denominator().full_mul(count), + ); + + try_dec256_into_dec(res) +} + +/// Same as [`safe_sma_calculation`] but is being used when buffer is not full yet. +/// Formula: (sma * count + new_price) / (count + 1) +pub fn safe_sma_buffer_not_full( + price_sma: Decimal, + count: u32, + new_price: Decimal, +) -> StdResult { + let sma_times_count = price_sma.numerator().full_mul(count); + let res = Decimal256::from_ratio( + sma_times_count + Uint256::from(new_price.numerator()), + price_sma.denominator().full_mul(count + 1), + ); + + try_dec256_into_dec(res) +} + +#[cfg(test)] +mod test { + use cosmwasm_std::to_json_binary; + + use crate::observation::Observation; + + #[test] + fn check_observation_size() { + // Checking [`Observation`] object size to estimate gas cost + + let obs = Observation { + ts: 0, + price: Default::default(), + price_sma: Default::default(), + }; + + let storage_bytes = to_json_binary(&obs).unwrap().len(); + assert_eq!(storage_bytes, 36); // in storage + + // https://github.com/cosmos/cosmos-sdk/blob/47f46643affd7ec7978329c42bac47275ac7e1cc/store/types/gas.go#L199 + println!("sdk gas cost per read {}", 1000 + storage_bytes * 3); + println!("sdk gas cost per write {}", 2000 + storage_bytes * 30) + } +} diff --git a/packages/palomadex/src/pair.rs b/packages/palomadex/src/pair.rs index 57d779b..fea9dbf 100644 --- a/packages/palomadex/src/pair.rs +++ b/packages/palomadex/src/pair.rs @@ -1,5 +1,8 @@ +#[allow(unused_imports)] +use crate::observation::OracleObservation; use cosmwasm_schema::{cw_serde, QueryResponses}; +#[allow(unused_imports)] use crate::asset::{Asset, AssetInfo, PairInfo}; use cosmwasm_std::{Addr, Binary, Decimal, Decimal256, Uint128, Uint64}; @@ -56,18 +59,6 @@ pub enum ExecuteMsg { }, /// Update the pair configuration UpdateConfig { params: Binary }, - /// ProposeNewOwner creates a proposal to change contract ownership. - /// The validity period for the proposal is set in the `expires_in` variable. - ProposeNewOwner { - /// Newly proposed contract owner - owner: String, - /// The date after which this proposal expires - expires_in: u64, - }, - /// DropOwnershipProposal removes the existing offer to change contract ownership. - DropOwnershipProposal {}, - /// Used to claim contract ownership. - ClaimOwnership {}, } /// This structure describes a CW20 hook message. @@ -127,6 +118,9 @@ pub enum QueryMsg { asset_info: AssetInfo, block_height: Uint64, }, + /// Query price from observations + #[returns(OracleObservation)] + Observe { seconds_ago: u64 }, } /// This struct is used to return a query result with the total amount of LP tokens and assets in a specific pool. @@ -193,11 +187,6 @@ pub struct CumulativePricesResponse { pub cumulative_prices: Vec<(AssetInfo, AssetInfo, Uint128)>, } -/// This structure describes a migration message. -/// We currently take no arguments for migrations. -#[cw_serde] -pub struct MigrateMsg {} - /// This structure holds XYK pool parameters. #[cw_serde] pub struct XYKPoolParams { diff --git a/packages/palomadex/src/querier.rs b/packages/palomadex/src/querier.rs index 5b8bb8f..001b788 100644 --- a/packages/palomadex/src/querier.rs +++ b/packages/palomadex/src/querier.rs @@ -197,7 +197,7 @@ pub fn query_pair_info( } /// Returns a vector that contains items of type [`PairInfo`] which -/// symbolize pairs instantiated in the Astroport factory +/// symbolize pairs instantiated in the Palomadex factory pub fn query_pairs_info( querier: &QuerierWrapper, factory_contract: impl Into, diff --git a/packages/palomadex/src/router.rs b/packages/palomadex/src/router.rs new file mode 100644 index 0000000..6303d7c --- /dev/null +++ b/packages/palomadex/src/router.rs @@ -0,0 +1,118 @@ +use cosmwasm_schema::{cw_serde, QueryResponses}; + +use cosmwasm_std::{Decimal, Uint128}; +use cw20::Cw20ReceiveMsg; + +use crate::asset::AssetInfo; + +pub const MAX_SWAP_OPERATIONS: usize = 50; + +/// This structure holds the parameters used for creating a contract. +#[cw_serde] +pub struct InstantiateMsg { + /// The palomadex factory contract address + pub palomadex_factory: String, +} + +/// This enum describes a swap operation. +#[cw_serde] +pub enum SwapOperation { + /// Native swap + NativeSwap { + /// The name (denomination) of the native asset to swap from + offer_denom: String, + /// The name (denomination) of the native asset to swap to + ask_denom: String, + }, + /// ASTRO swap + AstroSwap { + /// Information about the asset being swapped + offer_asset_info: AssetInfo, + /// Information about the asset we swap to + ask_asset_info: AssetInfo, + }, +} + +impl SwapOperation { + pub fn get_target_asset_info(&self) -> AssetInfo { + match self { + SwapOperation::NativeSwap { ask_denom, .. } => AssetInfo::NativeToken { + denom: ask_denom.clone(), + }, + SwapOperation::AstroSwap { ask_asset_info, .. } => ask_asset_info.clone(), + } + } +} + +/// This structure describes the execute messages available in the contract. +#[cw_serde] +pub enum ExecuteMsg { + /// Receive receives a message of type [`Cw20ReceiveMsg`] and processes it depending on the received template + Receive(Cw20ReceiveMsg), + /// ExecuteSwapOperations processes multiple swaps while mentioning the minimum amount of tokens to receive for the last swap operation + ExecuteSwapOperations { + operations: Vec, + minimum_receive: Option, + to: Option, + max_spread: Option, + }, + + /// Internal use + /// ExecuteSwapOperation executes a single swap operation + ExecuteSwapOperation { + operation: SwapOperation, + to: Option, + max_spread: Option, + single: bool, + }, +} + +#[cw_serde] +pub struct SwapResponseData { + pub return_amount: Uint128, +} + +#[cw_serde] +pub enum Cw20HookMsg { + ExecuteSwapOperations { + /// A vector of swap operations + operations: Vec, + /// The minimum amount of tokens to get from a swap + minimum_receive: Option, + /// The recipient + to: Option, + /// Max spread + max_spread: Option, + }, +} + +/// This structure describes the query messages available in the contract. +#[cw_serde] +#[derive(QueryResponses)] +pub enum QueryMsg { + /// Config returns configuration parameters for the contract using a custom [`ConfigResponse`] structure + #[returns(ConfigResponse)] + Config {}, + /// SimulateSwapOperations simulates multi-hop swap operations + #[returns(SimulateSwapOperationsResponse)] + SimulateSwapOperations { + /// The amount of tokens to swap + offer_amount: Uint128, + /// The swap operations to perform, each swap involving a specific pool + operations: Vec, + }, +} + +/// This structure describes a custom struct to return a query response containing the base contract configuration. +#[cw_serde] +pub struct ConfigResponse { + /// The Palomadex factory contract address + pub palomadex_factory: String, +} + +/// This structure describes a custom struct to return a query response containing the end amount of a swap simulation +#[cw_serde] +pub struct SimulateSwapOperationsResponse { + /// The amount of tokens received in a swap simulation + pub amount: Uint128, +} diff --git a/packages/palomadex_mocks/Cargo.toml b/packages/palomadex_mocks/Cargo.toml new file mode 100644 index 0000000..9d4829a --- /dev/null +++ b/packages/palomadex_mocks/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "palomadex-mocks" +version = "0.2.0" +authors = ["Volume Finance"] +edition = "2021" +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lints.rust] +unexpected_cfgs = { level = "warn", check-cfg = ["cfg(tarpaulin_include)"] } +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +palomadex = { path = "../palomadex" } +palomadex-factory = { path = "../../contracts/factory" } +palomadex-native-coin-registry = { path = "../native_coin_registry" } +palomadex-pair = { path = "../../contracts/pair" } +palomadex-pair-stable = { path = "../../contracts/pair_stable" } +cw20-base = "1" +cosmwasm-schema = "1.2.5" +cosmwasm-std = "1.2.5" +cw-multi-test = "1.2.0" +injective-cosmwasm = "0.2" +schemars = "0.8.1" +serde = "1.0" +cw-utils = "1.0" +cw20 = "0.15" +anyhow = "1.0" +cw3 = "1.0" +cw1-whitelist = { version = "1.1.2", features = ["library"] } diff --git a/packages/palomadex_mocks/src/coin_registry.rs b/packages/palomadex_mocks/src/coin_registry.rs new file mode 100644 index 0000000..e10b271 --- /dev/null +++ b/packages/palomadex_mocks/src/coin_registry.rs @@ -0,0 +1,127 @@ +use cosmwasm_std::{Addr, Api, CustomMsg, CustomQuery, Storage}; +use cw_multi_test::{ + AppResponse, Bank, ContractWrapper, Distribution, Executor, Gov, Ibc, Module, Staking, Stargate, +}; +use palomadex::native_coin_registry::{ExecuteMsg, InstantiateMsg}; +use serde::de::DeserializeOwned; + +use crate::{palomadex_address, WKApp, PALOMADEX}; + +pub fn store_code(app: &WKApp) -> u64 +where + B: Bank, + A: Api, + S: Storage, + C: Module, + X: Staking, + D: Distribution, + I: Ibc, + G: Gov, + T: Stargate, + C::ExecT: CustomMsg + DeserializeOwned + 'static, + C::QueryT: CustomQuery + DeserializeOwned + 'static, +{ + use palomadex_native_coin_registry as cnt; + let contract = Box::new(ContractWrapper::new_with_empty( + cnt::contract::execute, + cnt::contract::instantiate, + cnt::contract::query, + )); + + app.borrow_mut().store_code(contract) +} + +pub struct MockCoinRegistryBuilder { + pub app: WKApp, +} + +impl MockCoinRegistryBuilder +where + B: Bank, + A: Api, + S: Storage, + C: Module, + X: Staking, + D: Distribution, + I: Ibc, + G: Gov, + T: Stargate, + C::ExecT: CustomMsg + DeserializeOwned + 'static, + C::QueryT: CustomQuery + DeserializeOwned + 'static, +{ + pub fn new(app: &WKApp) -> Self { + Self { app: app.clone() } + } + pub fn instantiate(self) -> MockCoinRegistry { + let code_id = store_code(&self.app); + let palomadex = palomadex_address(); + + let address = self + .app + .borrow_mut() + .instantiate_contract( + code_id, + palomadex.clone(), + &InstantiateMsg { + owner: PALOMADEX.to_owned(), + }, + &[], + "Palomadex Coin Registry", + Some(PALOMADEX.to_owned()), + ) + .unwrap(); + + self.app + .borrow_mut() + .execute_contract( + palomadex, + address.clone(), + &ExecuteMsg::Add { + native_coins: vec![("ustake".to_owned(), 6), ("ucosmos".to_owned(), 6)], + }, + &[], + ) + .unwrap(); + + MockCoinRegistry { + app: self.app, + address, + } + } +} + +pub struct MockCoinRegistry { + pub app: WKApp, + pub address: Addr, +} + +impl MockCoinRegistry +where + B: Bank, + A: Api, + S: Storage, + C: Module, + X: Staking, + D: Distribution, + I: Ibc, + G: Gov, + T: Stargate, + C::ExecT: CustomMsg + DeserializeOwned + 'static, + C::QueryT: CustomQuery + DeserializeOwned + 'static, +{ + pub fn add(&self, coins: Vec<(String, u8)>) -> AppResponse { + let palomadex = palomadex_address(); + + self.app + .borrow_mut() + .execute_contract( + palomadex, + self.address.clone(), + &ExecuteMsg::Add { + native_coins: coins, + }, + &[], + ) + .unwrap() + } +} diff --git a/packages/palomadex_mocks/src/factory.rs b/packages/palomadex_mocks/src/factory.rs new file mode 100644 index 0000000..18532a7 --- /dev/null +++ b/packages/palomadex_mocks/src/factory.rs @@ -0,0 +1,294 @@ +use anyhow::Result as AnyResult; + +use cosmwasm_std::{to_json_binary, Addr, Api, CustomMsg, CustomQuery, Storage}; +use cw_multi_test::{ + AppResponse, Bank, ContractWrapper, Distribution, Executor, Gov, Ibc, Module, Staking, Stargate, +}; +use palomadex::{ + asset::{AssetInfo, PairInfo}, + factory::{ConfigResponse, ExecuteMsg, InstantiateMsg, PairConfig, PairType, QueryMsg}, + pair::StablePoolParams, +}; +use serde::de::DeserializeOwned; + +use crate::{ + palomadex_address, MockCoinRegistry, MockCoinRegistryBuilder, MockStablePair, MockXykPair, + WKApp, PALOMADEX, +}; + +pub fn store_code(app: &WKApp) -> u64 +where + B: Bank, + A: Api, + S: Storage, + C: Module, + X: Staking, + D: Distribution, + I: Ibc, + G: Gov, + T: Stargate, + C::ExecT: CustomMsg + DeserializeOwned + 'static, + C::QueryT: CustomQuery + DeserializeOwned + 'static, +{ + use palomadex_factory as cnt; + let contract = Box::new( + ContractWrapper::new_with_empty( + cnt::contract::execute, + cnt::contract::instantiate, + cnt::contract::query, + ) + .with_reply_empty(cnt::contract::reply), + ); + + app.borrow_mut().store_code(contract) +} + +pub struct MockFactoryBuilder { + pub app: WKApp, +} + +impl MockFactoryBuilder +where + B: Bank, + A: Api, + S: Storage, + C: Module, + X: Staking, + D: Distribution, + I: Ibc, + G: Gov, + T: Stargate, + C::ExecT: CustomMsg + DeserializeOwned + 'static, + C::QueryT: CustomQuery + DeserializeOwned + 'static, +{ + pub fn new(app: &WKApp) -> Self { + Self { app: app.clone() } + } + + pub fn instantiate(self) -> MockFactory { + let code_id = store_code(&self.app); + let palomadex = palomadex_address(); + + let xyk_code_id = crate::pair::store_code(&self.app); + let stable_code_id = crate::pair_stable::store_code(&self.app); + + let pair_configs = vec![ + PairConfig { + code_id: xyk_code_id, + pair_type: PairType::Xyk {}, + is_disabled: false, + is_generator_disabled: false, + total_fee_bps: 30, + maker_fee_bps: 3333, + permissioned: false, + }, + PairConfig { + code_id: stable_code_id, + pair_type: PairType::Stable {}, + is_disabled: false, + is_generator_disabled: false, + total_fee_bps: 5, + maker_fee_bps: 5000, + permissioned: false, + }, + ]; + + let token_code_id = crate::token::store_code(&self.app); + let whitelist_code_id = crate::whitelist::store_code(&self.app); + + let coin_registry = MockCoinRegistryBuilder::new(&self.app).instantiate(); + + let address = self + .app + .borrow_mut() + .instantiate_contract( + code_id, + palomadex, + &InstantiateMsg { + owner: PALOMADEX.to_owned(), + fee_address: None, + pair_configs, + token_code_id, + generator_address: None, + whitelist_code_id, + coin_registry_address: coin_registry.address.to_string(), + }, + &[], + "Palomadex Factory", + Some(PALOMADEX.to_owned()), + ) + .unwrap(); + + MockFactory { + app: self.app, + address, + } + } +} + +pub struct MockFactory { + pub app: WKApp, + pub address: Addr, +} + +pub type MockFactoryOpt = Option>; + +impl MockFactory +where + B: Bank, + A: Api, + S: Storage, + C: Module, + X: Staking, + D: Distribution, + I: Ibc, + G: Gov, + T: Stargate, + C::ExecT: CustomMsg + DeserializeOwned + 'static, + C::QueryT: CustomQuery + DeserializeOwned + 'static, +{ + pub fn whitelist_code_id(&self) -> u64 { + let config: ConfigResponse = self + .app + .borrow() + .wrap() + .query_wasm_smart(self.address.clone(), &QueryMsg::Config {}) + .unwrap(); + + config.whitelist_code_id + } + + pub fn token_code_id(&self) -> u64 { + let config: ConfigResponse = self + .app + .borrow() + .wrap() + .query_wasm_smart(self.address.clone(), &QueryMsg::Config {}) + .unwrap(); + + config.token_code_id + } + + pub fn instantiate_xyk_pair( + &self, + asset_infos: &[AssetInfo], + ) -> MockXykPair { + let palomadex = palomadex_address(); + + self.app + .borrow_mut() + .execute_contract( + palomadex, + self.address.clone(), + &ExecuteMsg::CreatePair { + pair_type: PairType::Xyk {}, + asset_infos: asset_infos.to_vec(), + init_params: None, + }, + &[], + ) + .unwrap(); + + let res: PairInfo = self + .app + .borrow() + .wrap() + .query_wasm_smart( + &self.address, + &QueryMsg::Pair { + asset_infos: asset_infos.to_vec(), + }, + ) + .unwrap(); + + MockXykPair { + app: self.app.clone(), + address: res.contract_addr, + } + } + + /// Set init_params to None to use the defaults + pub fn instantiate_stable_pair( + &self, + asset_infos: &[AssetInfo], + init_params: Option<&StablePoolParams>, + ) -> MockStablePair { + let palomadex = palomadex_address(); + + let default_params = StablePoolParams { + amp: 100, + owner: Some(palomadex.to_string()), + }; + + self.app + .borrow_mut() + .execute_contract( + palomadex, + self.address.clone(), + &ExecuteMsg::CreatePair { + pair_type: PairType::Stable {}, + asset_infos: asset_infos.to_vec(), + init_params: Some( + to_json_binary(init_params.unwrap_or(&default_params)).unwrap(), + ), + }, + &[], + ) + .unwrap(); + + let res: PairInfo = self + .app + .borrow() + .wrap() + .query_wasm_smart( + &self.address, + &QueryMsg::Pair { + asset_infos: asset_infos.to_vec(), + }, + ) + .unwrap(); + + MockStablePair { + app: self.app.clone(), + address: res.contract_addr, + } + } + + pub fn coin_registry(&self) -> MockCoinRegistry { + let config: ConfigResponse = self + .app + .borrow() + .wrap() + .query_wasm_smart(self.address.clone(), &QueryMsg::Config {}) + .unwrap(); + + MockCoinRegistry { + app: self.app.clone(), + address: config.coin_registry_address, + } + } + + pub fn deregister_pair(&self, asset_infos: &[AssetInfo]) -> AnyResult { + let palomadex = palomadex_address(); + + self.app.borrow_mut().execute_contract( + palomadex, + self.address.clone(), + &ExecuteMsg::Deregister { + asset_infos: asset_infos.to_vec(), + }, + &[], + ) + } + + pub fn config(&self) -> ConfigResponse { + let config: ConfigResponse = self + .app + .borrow() + .wrap() + .query_wasm_smart(self.address.clone(), &QueryMsg::Config {}) + .unwrap(); + + config + } +} diff --git a/packages/palomadex_mocks/src/lib.rs b/packages/palomadex_mocks/src/lib.rs new file mode 100644 index 0000000..34ef18c --- /dev/null +++ b/packages/palomadex_mocks/src/lib.rs @@ -0,0 +1,34 @@ +#![cfg(not(tarpaulin_include))] +use std::{cell::RefCell, rc::Rc}; + +use cosmwasm_std::Addr; +pub use cw_multi_test; + +use cw_multi_test::{App, Module, WasmKeeper}; + +pub use { + coin_registry::{MockCoinRegistry, MockCoinRegistryBuilder}, + factory::{MockFactory, MockFactoryBuilder}, + pair::{MockXykPair, MockXykPairBuilder}, + pair_stable::{MockStablePair, MockStablePairBuilder}, + token::{MockToken, MockTokenBuilder}, +}; + +pub mod coin_registry; +pub mod factory; +pub mod pair; +pub mod pair_stable; +pub mod token; +pub mod whitelist; + +pub const PALOMADEX: &str = "palomadex"; + +pub fn palomadex_address() -> Addr { + Addr::unchecked(PALOMADEX) +} +#[cfg(not(target_arch = "wasm32"))] +pub type WKApp = Rc< + RefCell< + App::ExecT, ::QueryT>, X, D, I, G, T>, + >, +>; diff --git a/packages/palomadex_mocks/src/pair.rs b/packages/palomadex_mocks/src/pair.rs new file mode 100644 index 0000000..5a3b25e --- /dev/null +++ b/packages/palomadex_mocks/src/pair.rs @@ -0,0 +1,178 @@ +use cosmwasm_std::{Addr, Api, Coin, CustomMsg, CustomQuery, Decimal, StdResult, Storage}; +use cw_multi_test::{ + Bank, ContractWrapper, Distribution, Executor, Gov, Ibc, Module, Staking, Stargate, +}; +use palomadex::{ + asset::{Asset, AssetInfo, PairInfo}, + pair::{ExecuteMsg, QueryMsg}, +}; +use serde::de::DeserializeOwned; + +use crate::{ + factory::{MockFactory, MockFactoryOpt}, + MockFactoryBuilder, MockToken, WKApp, +}; + +pub fn store_code(app: &WKApp) -> u64 +where + B: Bank, + A: Api, + S: Storage, + C: Module, + X: Staking, + D: Distribution, + I: Ibc, + G: Gov, + T: Stargate, + C::ExecT: CustomMsg + DeserializeOwned + 'static, + C::QueryT: CustomQuery + DeserializeOwned + 'static, +{ + use palomadex_pair as cnt; + let contract = Box::new( + ContractWrapper::new_with_empty( + cnt::contract::execute, + cnt::contract::instantiate, + cnt::contract::query, + ) + .with_reply_empty(cnt::contract::reply), + ); + + app.borrow_mut().store_code(contract) +} +pub struct MockXykPairBuilder { + pub app: WKApp, + pub asset_infos: Vec, + pub factory: MockFactoryOpt, +} + +impl MockXykPairBuilder +where + B: Bank, + A: Api, + S: Storage, + C: Module, + X: Staking, + D: Distribution, + I: Ibc, + G: Gov, + T: Stargate, + C::ExecT: CustomMsg + DeserializeOwned + 'static, + C::QueryT: CustomQuery + DeserializeOwned + 'static, +{ + pub fn new(app: &WKApp) -> Self { + Self { + app: app.clone(), + asset_infos: Default::default(), + factory: None, + } + } + + pub fn with_factory(mut self, factory: &MockFactory) -> Self { + self.factory = Some(MockFactory { + app: self.app.clone(), + address: factory.address.clone(), + }); + self + } + + pub fn with_asset(mut self, asset_info: &AssetInfo) -> Self { + self.asset_infos.push(asset_info.clone()); + self + } + + pub fn instantiate(self) -> MockXykPair { + let factory = self + .factory + .unwrap_or_else(|| MockFactoryBuilder::new(&self.app).instantiate()); + + factory.instantiate_xyk_pair(&self.asset_infos) + } +} + +#[derive(Clone)] +pub struct MockXykPair { + pub app: WKApp, + pub address: Addr, +} + +impl MockXykPair +where + B: Bank, + A: Api, + S: Storage, + C: Module, + X: Staking, + D: Distribution, + I: Ibc, + G: Gov, + T: Stargate, + C::ExecT: CustomMsg + DeserializeOwned + 'static, + C::QueryT: CustomQuery + DeserializeOwned + 'static, +{ + pub fn lp_token(&self) -> MockToken { + let res: PairInfo = self + .app + .borrow() + .wrap() + .query_wasm_smart(self.address.to_string(), &QueryMsg::Pair {}) + .unwrap(); + MockToken { + app: self.app.clone(), + address: res.liquidity_token, + } + } + + pub fn provide( + &self, + sender: &Addr, + assets: &[Asset], + slippage_tolerance: Option, + receiver: impl Into>, + ) { + let coins: Vec = assets + .iter() + .filter_map(|a| match &a.info { + AssetInfo::Token { .. } => None, + AssetInfo::NativeToken { denom } => Some(Coin { + denom: denom.clone(), + amount: a.amount, + }), + }) + .collect(); + + self.app + .borrow_mut() + .execute_contract( + sender.clone(), + self.address.clone(), + &ExecuteMsg::ProvideLiquidity { + assets: assets.into(), + slippage_tolerance, + receiver: receiver.into(), + }, + &coins, + ) + .unwrap(); + } + + pub fn mint_allow_provide_and_stake(&self, sender: &Addr, assets: &[Asset]) { + for asset in assets { + if let AssetInfo::Token { contract_addr } = &asset.info { + let token = MockToken { + app: self.app.clone(), + address: contract_addr.clone(), + }; + token.mint(sender, asset.amount); + token.allow(sender, &self.address, asset.amount); + }; + } + self.provide(sender, assets, None, None) + } + + pub fn pair_info(&self) -> StdResult { + self.app + .borrow() + .wrap() + .query_wasm_smart(self.address.clone(), &QueryMsg::Pair {}) + } +} diff --git a/packages/palomadex_mocks/src/pair_stable.rs b/packages/palomadex_mocks/src/pair_stable.rs new file mode 100644 index 0000000..ec09420 --- /dev/null +++ b/packages/palomadex_mocks/src/pair_stable.rs @@ -0,0 +1,147 @@ +use cosmwasm_std::{Addr, Api, CustomMsg, CustomQuery, Decimal, Storage}; +use cw_multi_test::{Bank, ContractWrapper, Distribution, Gov, Ibc, Module, Staking, Stargate}; +use palomadex::{ + asset::{Asset, AssetInfo, PairInfo}, + pair::{QueryMsg, StablePoolParams}, +}; +use serde::de::DeserializeOwned; + +use crate::{ + factory::MockFactoryOpt, MockFactory, MockFactoryBuilder, MockToken, MockXykPair, WKApp, +}; + +pub fn store_code(app: &WKApp) -> u64 +where + B: Bank, + A: Api, + S: Storage, + C: Module, + X: Staking, + D: Distribution, + I: Ibc, + G: Gov, + T: Stargate, + C::ExecT: CustomMsg + DeserializeOwned + 'static, + C::QueryT: CustomQuery + DeserializeOwned + 'static, +{ + use palomadex_pair_stable as cnt; + let contract = Box::new( + ContractWrapper::new_with_empty( + cnt::contract::execute, + cnt::contract::instantiate, + cnt::contract::query, + ) + .with_reply_empty(cnt::contract::reply), + ); + + app.borrow_mut().store_code(contract) +} + +pub struct MockStablePairBuilder { + pub app: WKApp, + pub asset_infos: Vec, + pub factory: MockFactoryOpt, +} + +impl MockStablePairBuilder +where + B: Bank, + A: Api, + S: Storage, + C: Module, + X: Staking, + D: Distribution, + I: Ibc, + G: Gov, + T: Stargate, + C::ExecT: CustomMsg + DeserializeOwned + 'static, + C::QueryT: CustomQuery + DeserializeOwned + 'static, +{ + pub fn new(app: &WKApp) -> Self { + Self { + app: app.clone(), + asset_infos: Default::default(), + factory: None, + } + } + + pub fn with_factory(mut self, factory: &MockFactory) -> Self { + self.factory = Some(MockFactory { + app: self.app.clone(), + address: factory.address.clone(), + }); + self + } + + pub fn with_asset(mut self, asset_info: &AssetInfo) -> Self { + self.asset_infos.push(asset_info.clone()); + self + } + + /// Set init_params to None to use the defaults + pub fn instantiate( + self, + params: Option<&StablePoolParams>, + ) -> MockStablePair { + let factory = self + .factory + .unwrap_or_else(|| MockFactoryBuilder::new(&self.app).instantiate()); + + factory.instantiate_stable_pair(&self.asset_infos, params) + } +} + +pub struct MockStablePair { + pub app: WKApp, + pub address: Addr, +} + +impl MockStablePair +where + B: Bank, + A: Api, + S: Storage, + C: Module, + X: Staking, + D: Distribution, + I: Ibc, + G: Gov, + T: Stargate, + C::ExecT: CustomMsg + DeserializeOwned + 'static, + C::QueryT: CustomQuery + DeserializeOwned + 'static, +{ + pub fn lp_token(&self) -> MockToken { + let res: PairInfo = self + .app + .borrow() + .wrap() + .query_wasm_smart(self.address.to_string(), &QueryMsg::Pair {}) + .unwrap(); + MockToken { + app: self.app.clone(), + address: res.liquidity_token, + } + } + + pub fn provide( + &self, + sender: &Addr, + assets: &[Asset], + slippage_tolerance: Option, + receiver: impl Into>, + ) { + let xyk = MockXykPair { + app: self.app.clone(), + address: self.address.clone(), + }; + xyk.provide(sender, assets, slippage_tolerance, receiver); + } + + pub fn mint_allow_provide_and_stake(&self, sender: &Addr, assets: &[Asset]) { + let xyk = MockXykPair { + app: self.app.clone(), + address: self.address.clone(), + }; + xyk.mint_allow_provide_and_stake(sender, assets); + } +} diff --git a/packages/palomadex_mocks/src/token.rs b/packages/palomadex_mocks/src/token.rs new file mode 100644 index 0000000..64aaaa6 --- /dev/null +++ b/packages/palomadex_mocks/src/token.rs @@ -0,0 +1,211 @@ +use cosmwasm_std::{Addr, Api, CustomMsg, CustomQuery, Storage, Uint128}; +use cw_multi_test::{ + Bank, ContractWrapper, Distribution, Executor, Gov, Ibc, Module, Staking, Stargate, +}; +use palomadex::{ + asset::AssetInfo, + token::{BalanceResponse, ExecuteMsg, InstantiateMsg, MinterResponse, QueryMsg}, +}; +use serde::de::DeserializeOwned; + +use crate::{palomadex_address, WKApp, PALOMADEX}; + +pub fn store_code(app: &WKApp) -> u64 +where + B: Bank, + A: Api, + S: Storage, + C: Module, + X: Staking, + D: Distribution, + I: Ibc, + G: Gov, + T: Stargate, + C::ExecT: CustomMsg + DeserializeOwned + 'static, + C::QueryT: CustomQuery + DeserializeOwned + 'static, +{ + use cw20_base as cnt; + let contract = Box::new(ContractWrapper::new_with_empty( + cnt::contract::execute, + cnt::contract::instantiate, + cnt::contract::query, + )); + + app.borrow_mut().store_code(contract) +} + +pub struct MockTokenBuilder { + pub app: WKApp, + pub symbol: String, +} + +impl MockTokenBuilder +where + B: Bank, + A: Api, + S: Storage, + C: Module, + X: Staking, + D: Distribution, + I: Ibc, + G: Gov, + T: Stargate, + C::ExecT: CustomMsg + DeserializeOwned + 'static, + C::QueryT: CustomQuery + DeserializeOwned + 'static, +{ + pub fn new(app: &WKApp, symbol: &str) -> Self { + Self { + app: app.clone(), + symbol: symbol.into(), + } + } + + pub fn instantiate(self) -> MockToken { + let code_id = store_code(&self.app); + let palomadex = palomadex_address(); + + let address = self + .app + .borrow_mut() + .instantiate_contract( + code_id, + palomadex, + &InstantiateMsg { + name: self.symbol.clone(), + mint: Some(MinterResponse { + minter: PALOMADEX.to_owned(), + cap: None, + }), + symbol: self.symbol.clone(), + decimals: 6, + marketing: None, + initial_balances: vec![], + }, + &[], + self.symbol, + Some(PALOMADEX.to_owned()), + ) + .unwrap(); + + MockToken { + app: self.app, + address, + } + } +} + +pub struct MockToken { + pub app: WKApp, + pub address: Addr, +} + +pub type MockTokenOpt = Option>; + +impl MockToken +where + B: Bank, + A: Api, + S: Storage, + C: Module, + X: Staking, + D: Distribution, + I: Ibc, + G: Gov, + T: Stargate, + C::ExecT: CustomMsg + DeserializeOwned + 'static, + C::QueryT: CustomQuery + DeserializeOwned + 'static, +{ + pub fn asset_info(&self) -> AssetInfo { + AssetInfo::Token { + contract_addr: self.address.clone(), + } + } + + pub fn mint(&self, recipient: &Addr, amount: Uint128) { + let palomadex = palomadex_address(); + self.app + .borrow_mut() + .execute_contract( + palomadex, + self.address.clone(), + &ExecuteMsg::Mint { + recipient: recipient.into(), + amount, + }, + &[], + ) + .unwrap(); + } + + pub fn balance(&self, address: &Addr) -> Uint128 { + let res: BalanceResponse = self + .app + .borrow() + .wrap() + .query_wasm_smart( + self.address.to_string(), + &QueryMsg::Balance { + address: address.into(), + }, + ) + .unwrap(); + + res.balance + } + + pub fn burn(&self, sender: &Addr, amount: Uint128) { + self.app + .borrow_mut() + .execute_contract( + sender.clone(), + self.address.clone(), + &ExecuteMsg::Burn { amount }, + &[], + ) + .unwrap(); + } + + pub fn allow(&self, sender: &Addr, spender: &Addr, amount: Uint128) { + self.app + .borrow_mut() + .execute_contract( + sender.clone(), + self.address.clone(), + &ExecuteMsg::IncreaseAllowance { + spender: spender.into(), + amount, + expires: None, + }, + &[], + ) + .unwrap(); + } +} +impl TryFrom<(&WKApp, &AssetInfo)> + for MockToken +where + B: Bank, + A: Api, + S: Storage, + C: Module, + X: Staking, + D: Distribution, + I: Ibc, + G: Gov, + T: Stargate, + C::ExecT: CustomMsg + DeserializeOwned + 'static, + C::QueryT: CustomQuery + DeserializeOwned + 'static, +{ + type Error = String; + fn try_from( + value: (&WKApp, &AssetInfo), + ) -> Result, Self::Error> { + match value.1 { + AssetInfo::Token { contract_addr } => Ok(MockToken { + app: value.0.clone(), + address: contract_addr.clone(), + }), + AssetInfo::NativeToken { denom } => Err(format!("{} is native coin!", denom)), + } + } +} diff --git a/packages/palomadex_mocks/src/whitelist.rs b/packages/palomadex_mocks/src/whitelist.rs new file mode 100644 index 0000000..f528b25 --- /dev/null +++ b/packages/palomadex_mocks/src/whitelist.rs @@ -0,0 +1,29 @@ +use cosmwasm_std::{Api, CustomMsg, CustomQuery, Storage}; +use cw_multi_test::{Bank, ContractWrapper, Distribution, Gov, Ibc, Module, Staking, Stargate}; +use serde::de::DeserializeOwned; + +use crate::WKApp; + +pub fn store_code(app: &WKApp) -> u64 +where + B: Bank, + A: Api, + S: Storage, + C: Module, + X: Staking, + D: Distribution, + I: Ibc, + G: Gov, + T: Stargate, + C::ExecT: CustomMsg + DeserializeOwned + 'static, + C::QueryT: CustomQuery + DeserializeOwned + 'static, +{ + use cw1_whitelist as cnt; + let contract = Box::new(ContractWrapper::new_with_empty( + cnt::contract::execute, + cnt::contract::instantiate, + cnt::contract::query, + )); + + app.borrow_mut().store_code(contract) +} diff --git a/schemas/palomadex-factory/palomadex-factory.json b/schemas/palomadex-factory/palomadex-factory.json new file mode 100644 index 0000000..80f132c --- /dev/null +++ b/schemas/palomadex-factory/palomadex-factory.json @@ -0,0 +1,1358 @@ +{ + "contract_name": "palomadex-factory", + "contract_version": "0.1.0", + "idl_version": "1.0.0", + "instantiate": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "InstantiateMsg", + "description": "This structure stores the basic settings for creating a new factory contract.", + "type": "object", + "required": [ + "coin_registry_address", + "owner", + "pair_configs", + "token_code_id", + "whitelist_code_id" + ], + "properties": { + "coin_registry_address": { + "description": "The address of the contract that contains the coins and their accuracy", + "type": "string" + }, + "fee_address": { + "description": "Contract address to send governance fees to (the Maker)", + "type": [ + "string", + "null" + ] + }, + "generator_address": { + "description": "Address of contract that is used to auto_stake LP tokens once someone provides liquidity in a pool", + "type": [ + "string", + "null" + ] + }, + "owner": { + "description": "Address of owner that is allowed to change factory contract parameters", + "type": "string" + }, + "pair_configs": { + "description": "IDs of contracts that are allowed to instantiate pairs", + "type": "array", + "items": { + "$ref": "#/definitions/PairConfig" + } + }, + "token_code_id": { + "description": "CW20 token contract code identifier", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "whitelist_code_id": { + "description": "CW1 whitelist contract code id used to store 3rd party rewards for staking Palomadex LP tokens", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false, + "definitions": { + "PairConfig": { + "description": "This structure stores a pair type's configuration.", + "type": "object", + "required": [ + "code_id", + "maker_fee_bps", + "pair_type", + "total_fee_bps" + ], + "properties": { + "code_id": { + "description": "ID of contract which is allowed to create pairs of this type", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "is_disabled": { + "description": "Whether a pair type is disabled or not. If it is disabled, new pairs cannot be created, but existing ones can still read the pair configuration Default is false.", + "default": false, + "type": "boolean" + }, + "is_generator_disabled": { + "description": "Setting this to true means that pairs of this type will not be able to get an ASTRO generator Default is false.", + "default": false, + "type": "boolean" + }, + "maker_fee_bps": { + "description": "The amount of fees (in bps) collected by the Maker contract from this pair type", + "type": "integer", + "format": "uint16", + "minimum": 0.0 + }, + "pair_type": { + "description": "The pair type (provided in a [`PairType`])", + "allOf": [ + { + "$ref": "#/definitions/PairType" + } + ] + }, + "permissioned": { + "description": "If pool type is permissioned, only factory owner can create pairs of this type. Default is false.", + "default": false, + "type": "boolean" + }, + "total_fee_bps": { + "description": "The total fees (in bps) charged by a pair of this type", + "type": "integer", + "format": "uint16", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + "PairType": { + "description": "This enum describes available pair types. ## Available pool types ``` # use palomadex::factory::PairType::{Custom, Stable, Xyk}; Xyk {}; Stable {}; Custom(String::from(\"Custom\")); ```", + "oneOf": [ + { + "description": "XYK pair type", + "type": "object", + "required": [ + "xyk" + ], + "properties": { + "xyk": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Stable pair type", + "type": "object", + "required": [ + "stable" + ], + "properties": { + "stable": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Custom pair type", + "type": "object", + "required": [ + "custom" + ], + "properties": { + "custom": { + "type": "string" + } + }, + "additionalProperties": false + } + ] + } + } + }, + "execute": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ExecuteMsg", + "description": "This structure describes the execute messages of the contract.", + "oneOf": [ + { + "description": "UpdateConfig updates relevant code IDs", + "type": "object", + "required": [ + "update_config" + ], + "properties": { + "update_config": { + "type": "object", + "properties": { + "coin_registry_address": { + "description": "The address of the contract that contains the coins and their accuracy", + "type": [ + "string", + "null" + ] + }, + "fee_address": { + "description": "Contract address to send governance fees to (the Maker)", + "type": [ + "string", + "null" + ] + }, + "generator_address": { + "description": "Contract address where Lp tokens can be auto_staked after someone provides liquidity in an incentivized Palomadex pool", + "type": [ + "string", + "null" + ] + }, + "token_code_id": { + "description": "CW20 token contract code identifier", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + }, + "whitelist_code_id": { + "description": "CW1 whitelist contract code id used to store 3rd party rewards for staking Palomadex LP tokens", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "UpdatePairConfig updates the config for a pair type.", + "type": "object", + "required": [ + "update_pair_config" + ], + "properties": { + "update_pair_config": { + "type": "object", + "required": [ + "config" + ], + "properties": { + "config": { + "description": "New [`PairConfig`] settings for a pair type", + "allOf": [ + { + "$ref": "#/definitions/PairConfig" + } + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "CreatePair instantiates a new pair contract.", + "type": "object", + "required": [ + "create_pair" + ], + "properties": { + "create_pair": { + "type": "object", + "required": [ + "asset_infos", + "pair_type" + ], + "properties": { + "asset_infos": { + "description": "The assets to create the pool for", + "type": "array", + "items": { + "$ref": "#/definitions/AssetInfo" + } + }, + "init_params": { + "description": "Optional binary serialised parameters for custom pool types", + "anyOf": [ + { + "$ref": "#/definitions/Binary" + }, + { + "type": "null" + } + ] + }, + "pair_type": { + "description": "The pair type (exposed in [`PairType`])", + "allOf": [ + { + "$ref": "#/definitions/PairType" + } + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Deregister removes a previously created pair.", + "type": "object", + "required": [ + "deregister" + ], + "properties": { + "deregister": { + "type": "object", + "required": [ + "asset_infos" + ], + "properties": { + "asset_infos": { + "description": "The assets for which we deregister a pool", + "type": "array", + "items": { + "$ref": "#/definitions/AssetInfo" + } + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "ProposeNewOwner creates a proposal to change contract ownership. The validity period for the proposal is set in the `expires_in` variable.", + "type": "object", + "required": [ + "propose_new_owner" + ], + "properties": { + "propose_new_owner": { + "type": "object", + "required": [ + "expires_in", + "owner" + ], + "properties": { + "expires_in": { + "description": "The date after which this proposal expires", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "owner": { + "description": "Newly proposed contract owner", + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "DropOwnershipProposal removes the existing offer to change contract ownership.", + "type": "object", + "required": [ + "drop_ownership_proposal" + ], + "properties": { + "drop_ownership_proposal": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Used to claim contract ownership.", + "type": "object", + "required": [ + "claim_ownership" + ], + "properties": { + "claim_ownership": { + "type": "object", + "additionalProperties": false + } + }, + "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" + }, + "AssetInfo": { + "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use palomadex::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", + "oneOf": [ + { + "description": "Non-native Token", + "type": "object", + "required": [ + "token" + ], + "properties": { + "token": { + "type": "object", + "required": [ + "contract_addr" + ], + "properties": { + "contract_addr": { + "$ref": "#/definitions/Addr" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Native token", + "type": "object", + "required": [ + "native_token" + ], + "properties": { + "native_token": { + "type": "object", + "required": [ + "denom" + ], + "properties": { + "denom": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "Binary": { + "description": "Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec. See also .", + "type": "string" + }, + "PairConfig": { + "description": "This structure stores a pair type's configuration.", + "type": "object", + "required": [ + "code_id", + "maker_fee_bps", + "pair_type", + "total_fee_bps" + ], + "properties": { + "code_id": { + "description": "ID of contract which is allowed to create pairs of this type", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "is_disabled": { + "description": "Whether a pair type is disabled or not. If it is disabled, new pairs cannot be created, but existing ones can still read the pair configuration Default is false.", + "default": false, + "type": "boolean" + }, + "is_generator_disabled": { + "description": "Setting this to true means that pairs of this type will not be able to get an ASTRO generator Default is false.", + "default": false, + "type": "boolean" + }, + "maker_fee_bps": { + "description": "The amount of fees (in bps) collected by the Maker contract from this pair type", + "type": "integer", + "format": "uint16", + "minimum": 0.0 + }, + "pair_type": { + "description": "The pair type (provided in a [`PairType`])", + "allOf": [ + { + "$ref": "#/definitions/PairType" + } + ] + }, + "permissioned": { + "description": "If pool type is permissioned, only factory owner can create pairs of this type. Default is false.", + "default": false, + "type": "boolean" + }, + "total_fee_bps": { + "description": "The total fees (in bps) charged by a pair of this type", + "type": "integer", + "format": "uint16", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + "PairType": { + "description": "This enum describes available pair types. ## Available pool types ``` # use palomadex::factory::PairType::{Custom, Stable, Xyk}; Xyk {}; Stable {}; Custom(String::from(\"Custom\")); ```", + "oneOf": [ + { + "description": "XYK pair type", + "type": "object", + "required": [ + "xyk" + ], + "properties": { + "xyk": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Stable pair type", + "type": "object", + "required": [ + "stable" + ], + "properties": { + "stable": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Custom pair type", + "type": "object", + "required": [ + "custom" + ], + "properties": { + "custom": { + "type": "string" + } + }, + "additionalProperties": false + } + ] + } + } + }, + "query": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "QueryMsg", + "description": "This structure describes the available query messages for the factory contract.", + "oneOf": [ + { + "description": "Config returns contract settings specified in the custom [`ConfigResponse`] structure.", + "type": "object", + "required": [ + "config" + ], + "properties": { + "config": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Pair returns information about a specific pair according to the specified assets.", + "type": "object", + "required": [ + "pair" + ], + "properties": { + "pair": { + "type": "object", + "required": [ + "asset_infos" + ], + "properties": { + "asset_infos": { + "description": "The assets for which we return a pair", + "type": "array", + "items": { + "$ref": "#/definitions/AssetInfo" + } + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Pairs returns an array of pairs and their information according to the specified parameters in `start_after` and `limit` variables.", + "type": "object", + "required": [ + "pairs" + ], + "properties": { + "pairs": { + "type": "object", + "properties": { + "limit": { + "description": "The number of pairs to read and return. It is an [`Option`] type.", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "description": "The pair item to start reading from. It is an [`Option`] type that accepts [`AssetInfo`] elements.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/AssetInfo" + } + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "FeeInfo returns fee parameters for a specific pair. The response is returned using a [`FeeInfoResponse`] structure", + "type": "object", + "required": [ + "fee_info" + ], + "properties": { + "fee_info": { + "type": "object", + "required": [ + "pair_type" + ], + "properties": { + "pair_type": { + "description": "The pair type for which we return fee information. Pair type is a [`PairType`] struct", + "allOf": [ + { + "$ref": "#/definitions/PairType" + } + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns a vector that contains blacklisted pair types", + "type": "object", + "required": [ + "blacklisted_pair_types" + ], + "properties": { + "blacklisted_pair_types": { + "type": "object", + "additionalProperties": false + } + }, + "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" + }, + "AssetInfo": { + "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use palomadex::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", + "oneOf": [ + { + "description": "Non-native Token", + "type": "object", + "required": [ + "token" + ], + "properties": { + "token": { + "type": "object", + "required": [ + "contract_addr" + ], + "properties": { + "contract_addr": { + "$ref": "#/definitions/Addr" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Native token", + "type": "object", + "required": [ + "native_token" + ], + "properties": { + "native_token": { + "type": "object", + "required": [ + "denom" + ], + "properties": { + "denom": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "PairType": { + "description": "This enum describes available pair types. ## Available pool types ``` # use palomadex::factory::PairType::{Custom, Stable, Xyk}; Xyk {}; Stable {}; Custom(String::from(\"Custom\")); ```", + "oneOf": [ + { + "description": "XYK pair type", + "type": "object", + "required": [ + "xyk" + ], + "properties": { + "xyk": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Stable pair type", + "type": "object", + "required": [ + "stable" + ], + "properties": { + "stable": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Custom pair type", + "type": "object", + "required": [ + "custom" + ], + "properties": { + "custom": { + "type": "string" + } + }, + "additionalProperties": false + } + ] + } + } + }, + "migrate": null, + "sudo": null, + "responses": { + "blacklisted_pair_types": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Array_of_PairType", + "type": "array", + "items": { + "$ref": "#/definitions/PairType" + }, + "definitions": { + "PairType": { + "description": "This enum describes available pair types. ## Available pool types ``` # use palomadex::factory::PairType::{Custom, Stable, Xyk}; Xyk {}; Stable {}; Custom(String::from(\"Custom\")); ```", + "oneOf": [ + { + "description": "XYK pair type", + "type": "object", + "required": [ + "xyk" + ], + "properties": { + "xyk": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Stable pair type", + "type": "object", + "required": [ + "stable" + ], + "properties": { + "stable": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Custom pair type", + "type": "object", + "required": [ + "custom" + ], + "properties": { + "custom": { + "type": "string" + } + }, + "additionalProperties": false + } + ] + } + } + }, + "config": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ConfigResponse", + "description": "A custom struct for each query response that returns general contract settings/configs.", + "type": "object", + "required": [ + "coin_registry_address", + "owner", + "pair_configs", + "token_code_id", + "whitelist_code_id" + ], + "properties": { + "coin_registry_address": { + "description": "The address of the contract that contains the coins and their accuracy", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + }, + "fee_address": { + "description": "Address of contract to send governance fees to (the Maker)", + "anyOf": [ + { + "$ref": "#/definitions/Addr" + }, + { + "type": "null" + } + ] + }, + "generator_address": { + "description": "Address of contract used to auto_stake LP tokens for Palomadex pairs that are incentivized", + "anyOf": [ + { + "$ref": "#/definitions/Addr" + }, + { + "type": "null" + } + ] + }, + "owner": { + "description": "Addres of owner that is allowed to change contract parameters", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + }, + "pair_configs": { + "description": "IDs of contracts which are allowed to create pairs", + "type": "array", + "items": { + "$ref": "#/definitions/PairConfig" + } + }, + "token_code_id": { + "description": "CW20 token contract code identifier", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "whitelist_code_id": { + "description": "CW1 whitelist contract code id used to store 3rd party rewards for staking Palomadex LP tokens", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "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" + }, + "PairConfig": { + "description": "This structure stores a pair type's configuration.", + "type": "object", + "required": [ + "code_id", + "maker_fee_bps", + "pair_type", + "total_fee_bps" + ], + "properties": { + "code_id": { + "description": "ID of contract which is allowed to create pairs of this type", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "is_disabled": { + "description": "Whether a pair type is disabled or not. If it is disabled, new pairs cannot be created, but existing ones can still read the pair configuration Default is false.", + "default": false, + "type": "boolean" + }, + "is_generator_disabled": { + "description": "Setting this to true means that pairs of this type will not be able to get an ASTRO generator Default is false.", + "default": false, + "type": "boolean" + }, + "maker_fee_bps": { + "description": "The amount of fees (in bps) collected by the Maker contract from this pair type", + "type": "integer", + "format": "uint16", + "minimum": 0.0 + }, + "pair_type": { + "description": "The pair type (provided in a [`PairType`])", + "allOf": [ + { + "$ref": "#/definitions/PairType" + } + ] + }, + "permissioned": { + "description": "If pool type is permissioned, only factory owner can create pairs of this type. Default is false.", + "default": false, + "type": "boolean" + }, + "total_fee_bps": { + "description": "The total fees (in bps) charged by a pair of this type", + "type": "integer", + "format": "uint16", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + "PairType": { + "description": "This enum describes available pair types. ## Available pool types ``` # use palomadex::factory::PairType::{Custom, Stable, Xyk}; Xyk {}; Stable {}; Custom(String::from(\"Custom\")); ```", + "oneOf": [ + { + "description": "XYK pair type", + "type": "object", + "required": [ + "xyk" + ], + "properties": { + "xyk": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Stable pair type", + "type": "object", + "required": [ + "stable" + ], + "properties": { + "stable": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Custom pair type", + "type": "object", + "required": [ + "custom" + ], + "properties": { + "custom": { + "type": "string" + } + }, + "additionalProperties": false + } + ] + } + } + }, + "fee_info": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "FeeInfoResponse", + "description": "A custom struct for each query response that returns an object of type [`FeeInfoResponse`].", + "type": "object", + "required": [ + "maker_fee_bps", + "total_fee_bps" + ], + "properties": { + "fee_address": { + "description": "Contract address to send governance fees to", + "anyOf": [ + { + "$ref": "#/definitions/Addr" + }, + { + "type": "null" + } + ] + }, + "maker_fee_bps": { + "description": "Amount of fees (in bps) sent to the Maker contract", + "type": "integer", + "format": "uint16", + "minimum": 0.0 + }, + "total_fee_bps": { + "description": "Total amount of fees (in bps) charged on a swap", + "type": "integer", + "format": "uint16", + "minimum": 0.0 + } + }, + "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" + } + } + }, + "pair": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "PairInfo", + "description": "This structure stores the main parameters for an palomadex pair", + "type": "object", + "required": [ + "asset_infos", + "contract_addr", + "liquidity_token", + "pair_type" + ], + "properties": { + "asset_infos": { + "description": "Asset information for the assets in the pool", + "type": "array", + "items": { + "$ref": "#/definitions/AssetInfo" + } + }, + "contract_addr": { + "description": "Pair contract address", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + }, + "liquidity_token": { + "description": "Pair LP token address", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + }, + "pair_type": { + "description": "The pool type (xyk, stableswap etc) available in [`PairType`]", + "allOf": [ + { + "$ref": "#/definitions/PairType" + } + ] + } + }, + "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" + }, + "AssetInfo": { + "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use palomadex::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", + "oneOf": [ + { + "description": "Non-native Token", + "type": "object", + "required": [ + "token" + ], + "properties": { + "token": { + "type": "object", + "required": [ + "contract_addr" + ], + "properties": { + "contract_addr": { + "$ref": "#/definitions/Addr" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Native token", + "type": "object", + "required": [ + "native_token" + ], + "properties": { + "native_token": { + "type": "object", + "required": [ + "denom" + ], + "properties": { + "denom": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "PairType": { + "description": "This enum describes available pair types. ## Available pool types ``` # use palomadex::factory::PairType::{Custom, Stable, Xyk}; Xyk {}; Stable {}; Custom(String::from(\"Custom\")); ```", + "oneOf": [ + { + "description": "XYK pair type", + "type": "object", + "required": [ + "xyk" + ], + "properties": { + "xyk": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Stable pair type", + "type": "object", + "required": [ + "stable" + ], + "properties": { + "stable": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Custom pair type", + "type": "object", + "required": [ + "custom" + ], + "properties": { + "custom": { + "type": "string" + } + }, + "additionalProperties": false + } + ] + } + } + }, + "pairs": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "PairsResponse", + "description": "A custom struct for each query response that returns an array of objects of type [`PairInfo`].", + "type": "object", + "required": [ + "pairs" + ], + "properties": { + "pairs": { + "description": "Arrays of structs containing information about multiple pairs", + "type": "array", + "items": { + "$ref": "#/definitions/PairInfo" + } + } + }, + "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" + }, + "AssetInfo": { + "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use palomadex::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", + "oneOf": [ + { + "description": "Non-native Token", + "type": "object", + "required": [ + "token" + ], + "properties": { + "token": { + "type": "object", + "required": [ + "contract_addr" + ], + "properties": { + "contract_addr": { + "$ref": "#/definitions/Addr" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Native token", + "type": "object", + "required": [ + "native_token" + ], + "properties": { + "native_token": { + "type": "object", + "required": [ + "denom" + ], + "properties": { + "denom": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "PairInfo": { + "description": "This structure stores the main parameters for an palomadex pair", + "type": "object", + "required": [ + "asset_infos", + "contract_addr", + "liquidity_token", + "pair_type" + ], + "properties": { + "asset_infos": { + "description": "Asset information for the assets in the pool", + "type": "array", + "items": { + "$ref": "#/definitions/AssetInfo" + } + }, + "contract_addr": { + "description": "Pair contract address", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + }, + "liquidity_token": { + "description": "Pair LP token address", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + }, + "pair_type": { + "description": "The pool type (xyk, stableswap etc) available in [`PairType`]", + "allOf": [ + { + "$ref": "#/definitions/PairType" + } + ] + } + }, + "additionalProperties": false + }, + "PairType": { + "description": "This enum describes available pair types. ## Available pool types ``` # use palomadex::factory::PairType::{Custom, Stable, Xyk}; Xyk {}; Stable {}; Custom(String::from(\"Custom\")); ```", + "oneOf": [ + { + "description": "XYK pair type", + "type": "object", + "required": [ + "xyk" + ], + "properties": { + "xyk": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Stable pair type", + "type": "object", + "required": [ + "stable" + ], + "properties": { + "stable": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Custom pair type", + "type": "object", + "required": [ + "custom" + ], + "properties": { + "custom": { + "type": "string" + } + }, + "additionalProperties": false + } + ] + } + } + } + } +} diff --git a/schemas/palomadex-factory/raw/execute.json b/schemas/palomadex-factory/raw/execute.json new file mode 100644 index 0000000..7e71874 --- /dev/null +++ b/schemas/palomadex-factory/raw/execute.json @@ -0,0 +1,378 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ExecuteMsg", + "description": "This structure describes the execute messages of the contract.", + "oneOf": [ + { + "description": "UpdateConfig updates relevant code IDs", + "type": "object", + "required": [ + "update_config" + ], + "properties": { + "update_config": { + "type": "object", + "properties": { + "coin_registry_address": { + "description": "The address of the contract that contains the coins and their accuracy", + "type": [ + "string", + "null" + ] + }, + "fee_address": { + "description": "Contract address to send governance fees to (the Maker)", + "type": [ + "string", + "null" + ] + }, + "generator_address": { + "description": "Contract address where Lp tokens can be auto_staked after someone provides liquidity in an incentivized Palomadex pool", + "type": [ + "string", + "null" + ] + }, + "token_code_id": { + "description": "CW20 token contract code identifier", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + }, + "whitelist_code_id": { + "description": "CW1 whitelist contract code id used to store 3rd party rewards for staking Palomadex LP tokens", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "UpdatePairConfig updates the config for a pair type.", + "type": "object", + "required": [ + "update_pair_config" + ], + "properties": { + "update_pair_config": { + "type": "object", + "required": [ + "config" + ], + "properties": { + "config": { + "description": "New [`PairConfig`] settings for a pair type", + "allOf": [ + { + "$ref": "#/definitions/PairConfig" + } + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "CreatePair instantiates a new pair contract.", + "type": "object", + "required": [ + "create_pair" + ], + "properties": { + "create_pair": { + "type": "object", + "required": [ + "asset_infos", + "pair_type" + ], + "properties": { + "asset_infos": { + "description": "The assets to create the pool for", + "type": "array", + "items": { + "$ref": "#/definitions/AssetInfo" + } + }, + "init_params": { + "description": "Optional binary serialised parameters for custom pool types", + "anyOf": [ + { + "$ref": "#/definitions/Binary" + }, + { + "type": "null" + } + ] + }, + "pair_type": { + "description": "The pair type (exposed in [`PairType`])", + "allOf": [ + { + "$ref": "#/definitions/PairType" + } + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Deregister removes a previously created pair.", + "type": "object", + "required": [ + "deregister" + ], + "properties": { + "deregister": { + "type": "object", + "required": [ + "asset_infos" + ], + "properties": { + "asset_infos": { + "description": "The assets for which we deregister a pool", + "type": "array", + "items": { + "$ref": "#/definitions/AssetInfo" + } + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "ProposeNewOwner creates a proposal to change contract ownership. The validity period for the proposal is set in the `expires_in` variable.", + "type": "object", + "required": [ + "propose_new_owner" + ], + "properties": { + "propose_new_owner": { + "type": "object", + "required": [ + "expires_in", + "owner" + ], + "properties": { + "expires_in": { + "description": "The date after which this proposal expires", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "owner": { + "description": "Newly proposed contract owner", + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "DropOwnershipProposal removes the existing offer to change contract ownership.", + "type": "object", + "required": [ + "drop_ownership_proposal" + ], + "properties": { + "drop_ownership_proposal": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Used to claim contract ownership.", + "type": "object", + "required": [ + "claim_ownership" + ], + "properties": { + "claim_ownership": { + "type": "object", + "additionalProperties": false + } + }, + "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" + }, + "AssetInfo": { + "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use palomadex::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", + "oneOf": [ + { + "description": "Non-native Token", + "type": "object", + "required": [ + "token" + ], + "properties": { + "token": { + "type": "object", + "required": [ + "contract_addr" + ], + "properties": { + "contract_addr": { + "$ref": "#/definitions/Addr" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Native token", + "type": "object", + "required": [ + "native_token" + ], + "properties": { + "native_token": { + "type": "object", + "required": [ + "denom" + ], + "properties": { + "denom": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "Binary": { + "description": "Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec. See also .", + "type": "string" + }, + "PairConfig": { + "description": "This structure stores a pair type's configuration.", + "type": "object", + "required": [ + "code_id", + "maker_fee_bps", + "pair_type", + "total_fee_bps" + ], + "properties": { + "code_id": { + "description": "ID of contract which is allowed to create pairs of this type", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "is_disabled": { + "description": "Whether a pair type is disabled or not. If it is disabled, new pairs cannot be created, but existing ones can still read the pair configuration Default is false.", + "default": false, + "type": "boolean" + }, + "is_generator_disabled": { + "description": "Setting this to true means that pairs of this type will not be able to get an ASTRO generator Default is false.", + "default": false, + "type": "boolean" + }, + "maker_fee_bps": { + "description": "The amount of fees (in bps) collected by the Maker contract from this pair type", + "type": "integer", + "format": "uint16", + "minimum": 0.0 + }, + "pair_type": { + "description": "The pair type (provided in a [`PairType`])", + "allOf": [ + { + "$ref": "#/definitions/PairType" + } + ] + }, + "permissioned": { + "description": "If pool type is permissioned, only factory owner can create pairs of this type. Default is false.", + "default": false, + "type": "boolean" + }, + "total_fee_bps": { + "description": "The total fees (in bps) charged by a pair of this type", + "type": "integer", + "format": "uint16", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + "PairType": { + "description": "This enum describes available pair types. ## Available pool types ``` # use palomadex::factory::PairType::{Custom, Stable, Xyk}; Xyk {}; Stable {}; Custom(String::from(\"Custom\")); ```", + "oneOf": [ + { + "description": "XYK pair type", + "type": "object", + "required": [ + "xyk" + ], + "properties": { + "xyk": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Stable pair type", + "type": "object", + "required": [ + "stable" + ], + "properties": { + "stable": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Custom pair type", + "type": "object", + "required": [ + "custom" + ], + "properties": { + "custom": { + "type": "string" + } + }, + "additionalProperties": false + } + ] + } + } +} diff --git a/schemas/palomadex-factory/raw/instantiate.json b/schemas/palomadex-factory/raw/instantiate.json new file mode 100644 index 0000000..b0b99f5 --- /dev/null +++ b/schemas/palomadex-factory/raw/instantiate.json @@ -0,0 +1,159 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "InstantiateMsg", + "description": "This structure stores the basic settings for creating a new factory contract.", + "type": "object", + "required": [ + "coin_registry_address", + "owner", + "pair_configs", + "token_code_id", + "whitelist_code_id" + ], + "properties": { + "coin_registry_address": { + "description": "The address of the contract that contains the coins and their accuracy", + "type": "string" + }, + "fee_address": { + "description": "Contract address to send governance fees to (the Maker)", + "type": [ + "string", + "null" + ] + }, + "generator_address": { + "description": "Address of contract that is used to auto_stake LP tokens once someone provides liquidity in a pool", + "type": [ + "string", + "null" + ] + }, + "owner": { + "description": "Address of owner that is allowed to change factory contract parameters", + "type": "string" + }, + "pair_configs": { + "description": "IDs of contracts that are allowed to instantiate pairs", + "type": "array", + "items": { + "$ref": "#/definitions/PairConfig" + } + }, + "token_code_id": { + "description": "CW20 token contract code identifier", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "whitelist_code_id": { + "description": "CW1 whitelist contract code id used to store 3rd party rewards for staking Palomadex LP tokens", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false, + "definitions": { + "PairConfig": { + "description": "This structure stores a pair type's configuration.", + "type": "object", + "required": [ + "code_id", + "maker_fee_bps", + "pair_type", + "total_fee_bps" + ], + "properties": { + "code_id": { + "description": "ID of contract which is allowed to create pairs of this type", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "is_disabled": { + "description": "Whether a pair type is disabled or not. If it is disabled, new pairs cannot be created, but existing ones can still read the pair configuration Default is false.", + "default": false, + "type": "boolean" + }, + "is_generator_disabled": { + "description": "Setting this to true means that pairs of this type will not be able to get an ASTRO generator Default is false.", + "default": false, + "type": "boolean" + }, + "maker_fee_bps": { + "description": "The amount of fees (in bps) collected by the Maker contract from this pair type", + "type": "integer", + "format": "uint16", + "minimum": 0.0 + }, + "pair_type": { + "description": "The pair type (provided in a [`PairType`])", + "allOf": [ + { + "$ref": "#/definitions/PairType" + } + ] + }, + "permissioned": { + "description": "If pool type is permissioned, only factory owner can create pairs of this type. Default is false.", + "default": false, + "type": "boolean" + }, + "total_fee_bps": { + "description": "The total fees (in bps) charged by a pair of this type", + "type": "integer", + "format": "uint16", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + "PairType": { + "description": "This enum describes available pair types. ## Available pool types ``` # use palomadex::factory::PairType::{Custom, Stable, Xyk}; Xyk {}; Stable {}; Custom(String::from(\"Custom\")); ```", + "oneOf": [ + { + "description": "XYK pair type", + "type": "object", + "required": [ + "xyk" + ], + "properties": { + "xyk": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Stable pair type", + "type": "object", + "required": [ + "stable" + ], + "properties": { + "stable": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Custom pair type", + "type": "object", + "required": [ + "custom" + ], + "properties": { + "custom": { + "type": "string" + } + }, + "additionalProperties": false + } + ] + } + } +} diff --git a/schemas/palomadex-factory/raw/query.json b/schemas/palomadex-factory/raw/query.json new file mode 100644 index 0000000..88266d3 --- /dev/null +++ b/schemas/palomadex-factory/raw/query.json @@ -0,0 +1,224 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "QueryMsg", + "description": "This structure describes the available query messages for the factory contract.", + "oneOf": [ + { + "description": "Config returns contract settings specified in the custom [`ConfigResponse`] structure.", + "type": "object", + "required": [ + "config" + ], + "properties": { + "config": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Pair returns information about a specific pair according to the specified assets.", + "type": "object", + "required": [ + "pair" + ], + "properties": { + "pair": { + "type": "object", + "required": [ + "asset_infos" + ], + "properties": { + "asset_infos": { + "description": "The assets for which we return a pair", + "type": "array", + "items": { + "$ref": "#/definitions/AssetInfo" + } + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Pairs returns an array of pairs and their information according to the specified parameters in `start_after` and `limit` variables.", + "type": "object", + "required": [ + "pairs" + ], + "properties": { + "pairs": { + "type": "object", + "properties": { + "limit": { + "description": "The number of pairs to read and return. It is an [`Option`] type.", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "description": "The pair item to start reading from. It is an [`Option`] type that accepts [`AssetInfo`] elements.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/AssetInfo" + } + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "FeeInfo returns fee parameters for a specific pair. The response is returned using a [`FeeInfoResponse`] structure", + "type": "object", + "required": [ + "fee_info" + ], + "properties": { + "fee_info": { + "type": "object", + "required": [ + "pair_type" + ], + "properties": { + "pair_type": { + "description": "The pair type for which we return fee information. Pair type is a [`PairType`] struct", + "allOf": [ + { + "$ref": "#/definitions/PairType" + } + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns a vector that contains blacklisted pair types", + "type": "object", + "required": [ + "blacklisted_pair_types" + ], + "properties": { + "blacklisted_pair_types": { + "type": "object", + "additionalProperties": false + } + }, + "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" + }, + "AssetInfo": { + "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use palomadex::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", + "oneOf": [ + { + "description": "Non-native Token", + "type": "object", + "required": [ + "token" + ], + "properties": { + "token": { + "type": "object", + "required": [ + "contract_addr" + ], + "properties": { + "contract_addr": { + "$ref": "#/definitions/Addr" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Native token", + "type": "object", + "required": [ + "native_token" + ], + "properties": { + "native_token": { + "type": "object", + "required": [ + "denom" + ], + "properties": { + "denom": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "PairType": { + "description": "This enum describes available pair types. ## Available pool types ``` # use palomadex::factory::PairType::{Custom, Stable, Xyk}; Xyk {}; Stable {}; Custom(String::from(\"Custom\")); ```", + "oneOf": [ + { + "description": "XYK pair type", + "type": "object", + "required": [ + "xyk" + ], + "properties": { + "xyk": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Stable pair type", + "type": "object", + "required": [ + "stable" + ], + "properties": { + "stable": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Custom pair type", + "type": "object", + "required": [ + "custom" + ], + "properties": { + "custom": { + "type": "string" + } + }, + "additionalProperties": false + } + ] + } + } +} diff --git a/schemas/palomadex-factory/raw/response_to_blacklisted_pair_types.json b/schemas/palomadex-factory/raw/response_to_blacklisted_pair_types.json new file mode 100644 index 0000000..66b6f19 --- /dev/null +++ b/schemas/palomadex-factory/raw/response_to_blacklisted_pair_types.json @@ -0,0 +1,56 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Array_of_PairType", + "type": "array", + "items": { + "$ref": "#/definitions/PairType" + }, + "definitions": { + "PairType": { + "description": "This enum describes available pair types. ## Available pool types ``` # use palomadex::factory::PairType::{Custom, Stable, Xyk}; Xyk {}; Stable {}; Custom(String::from(\"Custom\")); ```", + "oneOf": [ + { + "description": "XYK pair type", + "type": "object", + "required": [ + "xyk" + ], + "properties": { + "xyk": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Stable pair type", + "type": "object", + "required": [ + "stable" + ], + "properties": { + "stable": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Custom pair type", + "type": "object", + "required": [ + "custom" + ], + "properties": { + "custom": { + "type": "string" + } + }, + "additionalProperties": false + } + ] + } + } +} diff --git a/schemas/palomadex-factory/raw/response_to_config.json b/schemas/palomadex-factory/raw/response_to_config.json new file mode 100644 index 0000000..75edbce --- /dev/null +++ b/schemas/palomadex-factory/raw/response_to_config.json @@ -0,0 +1,179 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ConfigResponse", + "description": "A custom struct for each query response that returns general contract settings/configs.", + "type": "object", + "required": [ + "coin_registry_address", + "owner", + "pair_configs", + "token_code_id", + "whitelist_code_id" + ], + "properties": { + "coin_registry_address": { + "description": "The address of the contract that contains the coins and their accuracy", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + }, + "fee_address": { + "description": "Address of contract to send governance fees to (the Maker)", + "anyOf": [ + { + "$ref": "#/definitions/Addr" + }, + { + "type": "null" + } + ] + }, + "generator_address": { + "description": "Address of contract used to auto_stake LP tokens for Palomadex pairs that are incentivized", + "anyOf": [ + { + "$ref": "#/definitions/Addr" + }, + { + "type": "null" + } + ] + }, + "owner": { + "description": "Addres of owner that is allowed to change contract parameters", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + }, + "pair_configs": { + "description": "IDs of contracts which are allowed to create pairs", + "type": "array", + "items": { + "$ref": "#/definitions/PairConfig" + } + }, + "token_code_id": { + "description": "CW20 token contract code identifier", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "whitelist_code_id": { + "description": "CW1 whitelist contract code id used to store 3rd party rewards for staking Palomadex LP tokens", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "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" + }, + "PairConfig": { + "description": "This structure stores a pair type's configuration.", + "type": "object", + "required": [ + "code_id", + "maker_fee_bps", + "pair_type", + "total_fee_bps" + ], + "properties": { + "code_id": { + "description": "ID of contract which is allowed to create pairs of this type", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "is_disabled": { + "description": "Whether a pair type is disabled or not. If it is disabled, new pairs cannot be created, but existing ones can still read the pair configuration Default is false.", + "default": false, + "type": "boolean" + }, + "is_generator_disabled": { + "description": "Setting this to true means that pairs of this type will not be able to get an ASTRO generator Default is false.", + "default": false, + "type": "boolean" + }, + "maker_fee_bps": { + "description": "The amount of fees (in bps) collected by the Maker contract from this pair type", + "type": "integer", + "format": "uint16", + "minimum": 0.0 + }, + "pair_type": { + "description": "The pair type (provided in a [`PairType`])", + "allOf": [ + { + "$ref": "#/definitions/PairType" + } + ] + }, + "permissioned": { + "description": "If pool type is permissioned, only factory owner can create pairs of this type. Default is false.", + "default": false, + "type": "boolean" + }, + "total_fee_bps": { + "description": "The total fees (in bps) charged by a pair of this type", + "type": "integer", + "format": "uint16", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + "PairType": { + "description": "This enum describes available pair types. ## Available pool types ``` # use palomadex::factory::PairType::{Custom, Stable, Xyk}; Xyk {}; Stable {}; Custom(String::from(\"Custom\")); ```", + "oneOf": [ + { + "description": "XYK pair type", + "type": "object", + "required": [ + "xyk" + ], + "properties": { + "xyk": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Stable pair type", + "type": "object", + "required": [ + "stable" + ], + "properties": { + "stable": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Custom pair type", + "type": "object", + "required": [ + "custom" + ], + "properties": { + "custom": { + "type": "string" + } + }, + "additionalProperties": false + } + ] + } + } +} diff --git a/schemas/palomadex-factory/raw/response_to_fee_info.json b/schemas/palomadex-factory/raw/response_to_fee_info.json new file mode 100644 index 0000000..d2a0a98 --- /dev/null +++ b/schemas/palomadex-factory/raw/response_to_fee_info.json @@ -0,0 +1,42 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "FeeInfoResponse", + "description": "A custom struct for each query response that returns an object of type [`FeeInfoResponse`].", + "type": "object", + "required": [ + "maker_fee_bps", + "total_fee_bps" + ], + "properties": { + "fee_address": { + "description": "Contract address to send governance fees to", + "anyOf": [ + { + "$ref": "#/definitions/Addr" + }, + { + "type": "null" + } + ] + }, + "maker_fee_bps": { + "description": "Amount of fees (in bps) sent to the Maker contract", + "type": "integer", + "format": "uint16", + "minimum": 0.0 + }, + "total_fee_bps": { + "description": "Total amount of fees (in bps) charged on a swap", + "type": "integer", + "format": "uint16", + "minimum": 0.0 + } + }, + "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" + } + } +} diff --git a/schemas/palomadex-factory/raw/response_to_pair.json b/schemas/palomadex-factory/raw/response_to_pair.json new file mode 100644 index 0000000..bd4b3e5 --- /dev/null +++ b/schemas/palomadex-factory/raw/response_to_pair.json @@ -0,0 +1,147 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "PairInfo", + "description": "This structure stores the main parameters for an palomadex pair", + "type": "object", + "required": [ + "asset_infos", + "contract_addr", + "liquidity_token", + "pair_type" + ], + "properties": { + "asset_infos": { + "description": "Asset information for the assets in the pool", + "type": "array", + "items": { + "$ref": "#/definitions/AssetInfo" + } + }, + "contract_addr": { + "description": "Pair contract address", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + }, + "liquidity_token": { + "description": "Pair LP token address", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + }, + "pair_type": { + "description": "The pool type (xyk, stableswap etc) available in [`PairType`]", + "allOf": [ + { + "$ref": "#/definitions/PairType" + } + ] + } + }, + "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" + }, + "AssetInfo": { + "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use palomadex::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", + "oneOf": [ + { + "description": "Non-native Token", + "type": "object", + "required": [ + "token" + ], + "properties": { + "token": { + "type": "object", + "required": [ + "contract_addr" + ], + "properties": { + "contract_addr": { + "$ref": "#/definitions/Addr" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Native token", + "type": "object", + "required": [ + "native_token" + ], + "properties": { + "native_token": { + "type": "object", + "required": [ + "denom" + ], + "properties": { + "denom": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "PairType": { + "description": "This enum describes available pair types. ## Available pool types ``` # use palomadex::factory::PairType::{Custom, Stable, Xyk}; Xyk {}; Stable {}; Custom(String::from(\"Custom\")); ```", + "oneOf": [ + { + "description": "XYK pair type", + "type": "object", + "required": [ + "xyk" + ], + "properties": { + "xyk": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Stable pair type", + "type": "object", + "required": [ + "stable" + ], + "properties": { + "stable": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Custom pair type", + "type": "object", + "required": [ + "custom" + ], + "properties": { + "custom": { + "type": "string" + } + }, + "additionalProperties": false + } + ] + } + } +} diff --git a/schemas/palomadex-factory/raw/response_to_pairs.json b/schemas/palomadex-factory/raw/response_to_pairs.json new file mode 100644 index 0000000..448b5ce --- /dev/null +++ b/schemas/palomadex-factory/raw/response_to_pairs.json @@ -0,0 +1,164 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "PairsResponse", + "description": "A custom struct for each query response that returns an array of objects of type [`PairInfo`].", + "type": "object", + "required": [ + "pairs" + ], + "properties": { + "pairs": { + "description": "Arrays of structs containing information about multiple pairs", + "type": "array", + "items": { + "$ref": "#/definitions/PairInfo" + } + } + }, + "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" + }, + "AssetInfo": { + "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use palomadex::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", + "oneOf": [ + { + "description": "Non-native Token", + "type": "object", + "required": [ + "token" + ], + "properties": { + "token": { + "type": "object", + "required": [ + "contract_addr" + ], + "properties": { + "contract_addr": { + "$ref": "#/definitions/Addr" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Native token", + "type": "object", + "required": [ + "native_token" + ], + "properties": { + "native_token": { + "type": "object", + "required": [ + "denom" + ], + "properties": { + "denom": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "PairInfo": { + "description": "This structure stores the main parameters for an palomadex pair", + "type": "object", + "required": [ + "asset_infos", + "contract_addr", + "liquidity_token", + "pair_type" + ], + "properties": { + "asset_infos": { + "description": "Asset information for the assets in the pool", + "type": "array", + "items": { + "$ref": "#/definitions/AssetInfo" + } + }, + "contract_addr": { + "description": "Pair contract address", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + }, + "liquidity_token": { + "description": "Pair LP token address", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + }, + "pair_type": { + "description": "The pool type (xyk, stableswap etc) available in [`PairType`]", + "allOf": [ + { + "$ref": "#/definitions/PairType" + } + ] + } + }, + "additionalProperties": false + }, + "PairType": { + "description": "This enum describes available pair types. ## Available pool types ``` # use palomadex::factory::PairType::{Custom, Stable, Xyk}; Xyk {}; Stable {}; Custom(String::from(\"Custom\")); ```", + "oneOf": [ + { + "description": "XYK pair type", + "type": "object", + "required": [ + "xyk" + ], + "properties": { + "xyk": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Stable pair type", + "type": "object", + "required": [ + "stable" + ], + "properties": { + "stable": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Custom pair type", + "type": "object", + "required": [ + "custom" + ], + "properties": { + "custom": { + "type": "string" + } + }, + "additionalProperties": false + } + ] + } + } +} diff --git a/schemas/palomadex-pair-stable/palomadex-pair-stable.json b/schemas/palomadex-pair-stable/palomadex-pair-stable.json new file mode 100644 index 0000000..13fa0a8 --- /dev/null +++ b/schemas/palomadex-pair-stable/palomadex-pair-stable.json @@ -0,0 +1,1357 @@ +{ + "contract_name": "palomadex-pair-stable", + "contract_version": "0.1.0", + "idl_version": "1.0.0", + "instantiate": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "InstantiateMsg", + "description": "This structure describes the parameters used for creating a contract.", + "type": "object", + "required": [ + "asset_infos", + "factory_addr", + "token_code_id" + ], + "properties": { + "asset_infos": { + "description": "Information about assets in the pool", + "type": "array", + "items": { + "$ref": "#/definitions/AssetInfo" + } + }, + "factory_addr": { + "description": "The factory contract address", + "type": "string" + }, + "init_params": { + "description": "Optional binary serialised parameters for custom pool types", + "anyOf": [ + { + "$ref": "#/definitions/Binary" + }, + { + "type": "null" + } + ] + }, + "token_code_id": { + "description": "The token contract code ID used for the tokens in the pool", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "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" + }, + "AssetInfo": { + "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use palomadex::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", + "oneOf": [ + { + "description": "Non-native Token", + "type": "object", + "required": [ + "token" + ], + "properties": { + "token": { + "type": "object", + "required": [ + "contract_addr" + ], + "properties": { + "contract_addr": { + "$ref": "#/definitions/Addr" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Native token", + "type": "object", + "required": [ + "native_token" + ], + "properties": { + "native_token": { + "type": "object", + "required": [ + "denom" + ], + "properties": { + "denom": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "Binary": { + "description": "Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec. See also .", + "type": "string" + } + } + }, + "execute": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ExecuteMsg", + "description": "This structure describes the execute messages available in the contract.", + "oneOf": [ + { + "description": "Receives a message of type [`Cw20ReceiveMsg`]", + "type": "object", + "required": [ + "receive" + ], + "properties": { + "receive": { + "$ref": "#/definitions/Cw20ReceiveMsg" + } + }, + "additionalProperties": false + }, + { + "description": "ProvideLiquidity allows someone to provide liquidity in the pool", + "type": "object", + "required": [ + "provide_liquidity" + ], + "properties": { + "provide_liquidity": { + "type": "object", + "required": [ + "assets" + ], + "properties": { + "assets": { + "description": "The assets available in the pool", + "type": "array", + "items": { + "$ref": "#/definitions/Asset" + } + }, + "receiver": { + "description": "The receiver of LP tokens", + "type": [ + "string", + "null" + ] + }, + "slippage_tolerance": { + "description": "The slippage tolerance that allows liquidity provision only if the price in the pool doesn't move too much", + "anyOf": [ + { + "$ref": "#/definitions/Decimal" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Swap performs a swap in the pool", + "type": "object", + "required": [ + "swap" + ], + "properties": { + "swap": { + "type": "object", + "required": [ + "offer_asset" + ], + "properties": { + "ask_asset_info": { + "anyOf": [ + { + "$ref": "#/definitions/AssetInfo" + }, + { + "type": "null" + } + ] + }, + "belief_price": { + "anyOf": [ + { + "$ref": "#/definitions/Decimal" + }, + { + "type": "null" + } + ] + }, + "max_spread": { + "anyOf": [ + { + "$ref": "#/definitions/Decimal" + }, + { + "type": "null" + } + ] + }, + "offer_asset": { + "$ref": "#/definitions/Asset" + }, + "to": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Update the pair configuration", + "type": "object", + "required": [ + "update_config" + ], + "properties": { + "update_config": { + "type": "object", + "required": [ + "params" + ], + "properties": { + "params": { + "$ref": "#/definitions/Binary" + } + }, + "additionalProperties": false + } + }, + "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" + }, + "Asset": { + "description": "This enum describes a Terra asset (native or CW20).", + "type": "object", + "required": [ + "amount", + "info" + ], + "properties": { + "amount": { + "description": "A token amount", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] + }, + "info": { + "description": "Information about an asset stored in a [`AssetInfo`] struct", + "allOf": [ + { + "$ref": "#/definitions/AssetInfo" + } + ] + } + }, + "additionalProperties": false + }, + "AssetInfo": { + "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use palomadex::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", + "oneOf": [ + { + "description": "Non-native Token", + "type": "object", + "required": [ + "token" + ], + "properties": { + "token": { + "type": "object", + "required": [ + "contract_addr" + ], + "properties": { + "contract_addr": { + "$ref": "#/definitions/Addr" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Native token", + "type": "object", + "required": [ + "native_token" + ], + "properties": { + "native_token": { + "type": "object", + "required": [ + "denom" + ], + "properties": { + "denom": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "Binary": { + "description": "Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec. See also .", + "type": "string" + }, + "Cw20ReceiveMsg": { + "description": "Cw20ReceiveMsg should be de/serialized under `Receive()` variant in a ExecuteMsg", + "type": "object", + "required": [ + "amount", + "msg", + "sender" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "msg": { + "$ref": "#/definitions/Binary" + }, + "sender": { + "type": "string" + } + }, + "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" + }, + "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" + } + } + }, + "query": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "QueryMsg", + "description": "This structure describes the query messages available in the contract.", + "oneOf": [ + { + "description": "Returns information about a pair in an object of type [`super::asset::PairInfo`].", + "type": "object", + "required": [ + "pair" + ], + "properties": { + "pair": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns information about a pool in an object of type [`PoolResponse`].", + "type": "object", + "required": [ + "pool" + ], + "properties": { + "pool": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns contract configuration settings in a custom [`ConfigResponse`] structure.", + "type": "object", + "required": [ + "config" + ], + "properties": { + "config": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns information about the share of the pool in a vector that contains objects of type [`Asset`].", + "type": "object", + "required": [ + "share" + ], + "properties": { + "share": { + "type": "object", + "required": [ + "amount" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns information about a swap simulation in a [`SimulationResponse`] object.", + "type": "object", + "required": [ + "simulation" + ], + "properties": { + "simulation": { + "type": "object", + "required": [ + "offer_asset" + ], + "properties": { + "ask_asset_info": { + "anyOf": [ + { + "$ref": "#/definitions/AssetInfo" + }, + { + "type": "null" + } + ] + }, + "offer_asset": { + "$ref": "#/definitions/Asset" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns information about cumulative prices in a [`ReverseSimulationResponse`] object.", + "type": "object", + "required": [ + "reverse_simulation" + ], + "properties": { + "reverse_simulation": { + "type": "object", + "required": [ + "ask_asset" + ], + "properties": { + "ask_asset": { + "$ref": "#/definitions/Asset" + }, + "offer_asset_info": { + "anyOf": [ + { + "$ref": "#/definitions/AssetInfo" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns information about the cumulative prices in a [`CumulativePricesResponse`] object", + "type": "object", + "required": [ + "cumulative_prices" + ], + "properties": { + "cumulative_prices": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns current D invariant in as a [`u128`] value", + "type": "object", + "required": [ + "query_compute_d" + ], + "properties": { + "query_compute_d": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns the balance of the specified asset that was in the pool just preceeding the moment of the specified block height creation.", + "type": "object", + "required": [ + "asset_balance_at" + ], + "properties": { + "asset_balance_at": { + "type": "object", + "required": [ + "asset_info", + "block_height" + ], + "properties": { + "asset_info": { + "$ref": "#/definitions/AssetInfo" + }, + "block_height": { + "$ref": "#/definitions/Uint64" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Query price from observations", + "type": "object", + "required": [ + "observe" + ], + "properties": { + "observe": { + "type": "object", + "required": [ + "seconds_ago" + ], + "properties": { + "seconds_ago": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "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" + }, + "Asset": { + "description": "This enum describes a Terra asset (native or CW20).", + "type": "object", + "required": [ + "amount", + "info" + ], + "properties": { + "amount": { + "description": "A token amount", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] + }, + "info": { + "description": "Information about an asset stored in a [`AssetInfo`] struct", + "allOf": [ + { + "$ref": "#/definitions/AssetInfo" + } + ] + } + }, + "additionalProperties": false + }, + "AssetInfo": { + "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use palomadex::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", + "oneOf": [ + { + "description": "Non-native Token", + "type": "object", + "required": [ + "token" + ], + "properties": { + "token": { + "type": "object", + "required": [ + "contract_addr" + ], + "properties": { + "contract_addr": { + "$ref": "#/definitions/Addr" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Native token", + "type": "object", + "required": [ + "native_token" + ], + "properties": { + "native_token": { + "type": "object", + "required": [ + "denom" + ], + "properties": { + "denom": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "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" + } + } + }, + "migrate": null, + "sudo": null, + "responses": { + "asset_balance_at": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Nullable_Uint128", + "anyOf": [ + { + "$ref": "#/definitions/Uint128" + }, + { + "type": "null" + } + ], + "definitions": { + "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" + } + } + }, + "config": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ConfigResponse", + "description": "This struct is used to return a query result with the general contract configuration.", + "type": "object", + "required": [ + "block_time_last", + "factory_addr", + "owner" + ], + "properties": { + "block_time_last": { + "description": "Last timestamp when the cumulative prices in the pool were updated", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "factory_addr": { + "description": "The factory contract address", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + }, + "owner": { + "description": "The contract owner", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + }, + "params": { + "description": "The pool's parameters", + "anyOf": [ + { + "$ref": "#/definitions/Binary" + }, + { + "type": "null" + } + ] + } + }, + "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" + }, + "Binary": { + "description": "Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec. See also .", + "type": "string" + } + } + }, + "cumulative_prices": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "CumulativePricesResponse", + "description": "This structure is used to return a cumulative prices query response.", + "type": "object", + "required": [ + "assets", + "cumulative_prices", + "total_share" + ], + "properties": { + "assets": { + "description": "The assets in the pool to query", + "type": "array", + "items": { + "$ref": "#/definitions/Asset" + } + }, + "cumulative_prices": { + "description": "The vector contains cumulative prices for each pair of assets in the pool", + "type": "array", + "items": { + "type": "array", + "items": [ + { + "$ref": "#/definitions/AssetInfo" + }, + { + "$ref": "#/definitions/AssetInfo" + }, + { + "$ref": "#/definitions/Uint128" + } + ], + "maxItems": 3, + "minItems": 3 + } + }, + "total_share": { + "description": "The total amount of LP tokens currently issued", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] + } + }, + "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" + }, + "Asset": { + "description": "This enum describes a Terra asset (native or CW20).", + "type": "object", + "required": [ + "amount", + "info" + ], + "properties": { + "amount": { + "description": "A token amount", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] + }, + "info": { + "description": "Information about an asset stored in a [`AssetInfo`] struct", + "allOf": [ + { + "$ref": "#/definitions/AssetInfo" + } + ] + } + }, + "additionalProperties": false + }, + "AssetInfo": { + "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use palomadex::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", + "oneOf": [ + { + "description": "Non-native Token", + "type": "object", + "required": [ + "token" + ], + "properties": { + "token": { + "type": "object", + "required": [ + "contract_addr" + ], + "properties": { + "contract_addr": { + "$ref": "#/definitions/Addr" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Native token", + "type": "object", + "required": [ + "native_token" + ], + "properties": { + "native_token": { + "type": "object", + "required": [ + "denom" + ], + "properties": { + "denom": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "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" + } + } + }, + "observe": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "OracleObservation", + "type": "object", + "required": [ + "price", + "timestamp" + ], + "properties": { + "price": { + "$ref": "#/definitions/Decimal" + }, + "timestamp": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false, + "definitions": { + "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" + } + } + }, + "pair": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "PairInfo", + "description": "This structure stores the main parameters for an palomadex pair", + "type": "object", + "required": [ + "asset_infos", + "contract_addr", + "liquidity_token", + "pair_type" + ], + "properties": { + "asset_infos": { + "description": "Asset information for the assets in the pool", + "type": "array", + "items": { + "$ref": "#/definitions/AssetInfo" + } + }, + "contract_addr": { + "description": "Pair contract address", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + }, + "liquidity_token": { + "description": "Pair LP token address", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + }, + "pair_type": { + "description": "The pool type (xyk, stableswap etc) available in [`PairType`]", + "allOf": [ + { + "$ref": "#/definitions/PairType" + } + ] + } + }, + "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" + }, + "AssetInfo": { + "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use palomadex::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", + "oneOf": [ + { + "description": "Non-native Token", + "type": "object", + "required": [ + "token" + ], + "properties": { + "token": { + "type": "object", + "required": [ + "contract_addr" + ], + "properties": { + "contract_addr": { + "$ref": "#/definitions/Addr" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Native token", + "type": "object", + "required": [ + "native_token" + ], + "properties": { + "native_token": { + "type": "object", + "required": [ + "denom" + ], + "properties": { + "denom": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "PairType": { + "description": "This enum describes available pair types. ## Available pool types ``` # use palomadex::factory::PairType::{Custom, Stable, Xyk}; Xyk {}; Stable {}; Custom(String::from(\"Custom\")); ```", + "oneOf": [ + { + "description": "XYK pair type", + "type": "object", + "required": [ + "xyk" + ], + "properties": { + "xyk": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Stable pair type", + "type": "object", + "required": [ + "stable" + ], + "properties": { + "stable": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Custom pair type", + "type": "object", + "required": [ + "custom" + ], + "properties": { + "custom": { + "type": "string" + } + }, + "additionalProperties": false + } + ] + } + } + }, + "pool": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "PoolResponse", + "description": "This struct is used to return a query result with the total amount of LP tokens and assets in a specific pool.", + "type": "object", + "required": [ + "assets", + "total_share" + ], + "properties": { + "assets": { + "description": "The assets in the pool together with asset amounts", + "type": "array", + "items": { + "$ref": "#/definitions/Asset" + } + }, + "total_share": { + "description": "The total amount of LP tokens currently issued", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] + } + }, + "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" + }, + "Asset": { + "description": "This enum describes a Terra asset (native or CW20).", + "type": "object", + "required": [ + "amount", + "info" + ], + "properties": { + "amount": { + "description": "A token amount", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] + }, + "info": { + "description": "Information about an asset stored in a [`AssetInfo`] struct", + "allOf": [ + { + "$ref": "#/definitions/AssetInfo" + } + ] + } + }, + "additionalProperties": false + }, + "AssetInfo": { + "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use palomadex::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", + "oneOf": [ + { + "description": "Non-native Token", + "type": "object", + "required": [ + "token" + ], + "properties": { + "token": { + "type": "object", + "required": [ + "contract_addr" + ], + "properties": { + "contract_addr": { + "$ref": "#/definitions/Addr" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Native token", + "type": "object", + "required": [ + "native_token" + ], + "properties": { + "native_token": { + "type": "object", + "required": [ + "denom" + ], + "properties": { + "denom": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "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" + } + } + }, + "query_compute_d": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "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" + }, + "reverse_simulation": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ReverseSimulationResponse", + "description": "This structure holds the parameters that are returned from a reverse swap simulation response.", + "type": "object", + "required": [ + "commission_amount", + "offer_amount", + "spread_amount" + ], + "properties": { + "commission_amount": { + "description": "The amount of fees charged by the transaction", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] + }, + "offer_amount": { + "description": "The amount of offer assets returned by the reverse swap", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] + }, + "spread_amount": { + "description": "The spread used in the swap operation", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] + } + }, + "additionalProperties": false, + "definitions": { + "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" + } + } + }, + "share": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Array_of_Asset", + "type": "array", + "items": { + "$ref": "#/definitions/Asset" + }, + "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" + }, + "Asset": { + "description": "This enum describes a Terra asset (native or CW20).", + "type": "object", + "required": [ + "amount", + "info" + ], + "properties": { + "amount": { + "description": "A token amount", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] + }, + "info": { + "description": "Information about an asset stored in a [`AssetInfo`] struct", + "allOf": [ + { + "$ref": "#/definitions/AssetInfo" + } + ] + } + }, + "additionalProperties": false + }, + "AssetInfo": { + "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use palomadex::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", + "oneOf": [ + { + "description": "Non-native Token", + "type": "object", + "required": [ + "token" + ], + "properties": { + "token": { + "type": "object", + "required": [ + "contract_addr" + ], + "properties": { + "contract_addr": { + "$ref": "#/definitions/Addr" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Native token", + "type": "object", + "required": [ + "native_token" + ], + "properties": { + "native_token": { + "type": "object", + "required": [ + "denom" + ], + "properties": { + "denom": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "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" + } + } + }, + "simulation": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "SimulationResponse", + "description": "This structure holds the parameters that are returned from a swap simulation response", + "type": "object", + "required": [ + "commission_amount", + "return_amount", + "spread_amount" + ], + "properties": { + "commission_amount": { + "description": "The amount of fees charged by the transaction", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] + }, + "return_amount": { + "description": "The amount of ask assets returned by the swap", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] + }, + "spread_amount": { + "description": "The spread used in the swap operation", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] + } + }, + "additionalProperties": false, + "definitions": { + "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" + } + } + } + } +} diff --git a/schemas/palomadex-pair-stable/raw/execute.json b/schemas/palomadex-pair-stable/raw/execute.json new file mode 100644 index 0000000..0cd254e --- /dev/null +++ b/schemas/palomadex-pair-stable/raw/execute.json @@ -0,0 +1,259 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ExecuteMsg", + "description": "This structure describes the execute messages available in the contract.", + "oneOf": [ + { + "description": "Receives a message of type [`Cw20ReceiveMsg`]", + "type": "object", + "required": [ + "receive" + ], + "properties": { + "receive": { + "$ref": "#/definitions/Cw20ReceiveMsg" + } + }, + "additionalProperties": false + }, + { + "description": "ProvideLiquidity allows someone to provide liquidity in the pool", + "type": "object", + "required": [ + "provide_liquidity" + ], + "properties": { + "provide_liquidity": { + "type": "object", + "required": [ + "assets" + ], + "properties": { + "assets": { + "description": "The assets available in the pool", + "type": "array", + "items": { + "$ref": "#/definitions/Asset" + } + }, + "receiver": { + "description": "The receiver of LP tokens", + "type": [ + "string", + "null" + ] + }, + "slippage_tolerance": { + "description": "The slippage tolerance that allows liquidity provision only if the price in the pool doesn't move too much", + "anyOf": [ + { + "$ref": "#/definitions/Decimal" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Swap performs a swap in the pool", + "type": "object", + "required": [ + "swap" + ], + "properties": { + "swap": { + "type": "object", + "required": [ + "offer_asset" + ], + "properties": { + "ask_asset_info": { + "anyOf": [ + { + "$ref": "#/definitions/AssetInfo" + }, + { + "type": "null" + } + ] + }, + "belief_price": { + "anyOf": [ + { + "$ref": "#/definitions/Decimal" + }, + { + "type": "null" + } + ] + }, + "max_spread": { + "anyOf": [ + { + "$ref": "#/definitions/Decimal" + }, + { + "type": "null" + } + ] + }, + "offer_asset": { + "$ref": "#/definitions/Asset" + }, + "to": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Update the pair configuration", + "type": "object", + "required": [ + "update_config" + ], + "properties": { + "update_config": { + "type": "object", + "required": [ + "params" + ], + "properties": { + "params": { + "$ref": "#/definitions/Binary" + } + }, + "additionalProperties": false + } + }, + "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" + }, + "Asset": { + "description": "This enum describes a Terra asset (native or CW20).", + "type": "object", + "required": [ + "amount", + "info" + ], + "properties": { + "amount": { + "description": "A token amount", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] + }, + "info": { + "description": "Information about an asset stored in a [`AssetInfo`] struct", + "allOf": [ + { + "$ref": "#/definitions/AssetInfo" + } + ] + } + }, + "additionalProperties": false + }, + "AssetInfo": { + "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use palomadex::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", + "oneOf": [ + { + "description": "Non-native Token", + "type": "object", + "required": [ + "token" + ], + "properties": { + "token": { + "type": "object", + "required": [ + "contract_addr" + ], + "properties": { + "contract_addr": { + "$ref": "#/definitions/Addr" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Native token", + "type": "object", + "required": [ + "native_token" + ], + "properties": { + "native_token": { + "type": "object", + "required": [ + "denom" + ], + "properties": { + "denom": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "Binary": { + "description": "Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec. See also .", + "type": "string" + }, + "Cw20ReceiveMsg": { + "description": "Cw20ReceiveMsg should be de/serialized under `Receive()` variant in a ExecuteMsg", + "type": "object", + "required": [ + "amount", + "msg", + "sender" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "msg": { + "$ref": "#/definitions/Binary" + }, + "sender": { + "type": "string" + } + }, + "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" + }, + "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" + } + } +} diff --git a/schemas/palomadex-pair-stable/raw/instantiate.json b/schemas/palomadex-pair-stable/raw/instantiate.json new file mode 100644 index 0000000..4782a48 --- /dev/null +++ b/schemas/palomadex-pair-stable/raw/instantiate.json @@ -0,0 +1,101 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "InstantiateMsg", + "description": "This structure describes the parameters used for creating a contract.", + "type": "object", + "required": [ + "asset_infos", + "factory_addr", + "token_code_id" + ], + "properties": { + "asset_infos": { + "description": "Information about assets in the pool", + "type": "array", + "items": { + "$ref": "#/definitions/AssetInfo" + } + }, + "factory_addr": { + "description": "The factory contract address", + "type": "string" + }, + "init_params": { + "description": "Optional binary serialised parameters for custom pool types", + "anyOf": [ + { + "$ref": "#/definitions/Binary" + }, + { + "type": "null" + } + ] + }, + "token_code_id": { + "description": "The token contract code ID used for the tokens in the pool", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "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" + }, + "AssetInfo": { + "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use palomadex::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", + "oneOf": [ + { + "description": "Non-native Token", + "type": "object", + "required": [ + "token" + ], + "properties": { + "token": { + "type": "object", + "required": [ + "contract_addr" + ], + "properties": { + "contract_addr": { + "$ref": "#/definitions/Addr" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Native token", + "type": "object", + "required": [ + "native_token" + ], + "properties": { + "native_token": { + "type": "object", + "required": [ + "denom" + ], + "properties": { + "denom": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "Binary": { + "description": "Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec. See also .", + "type": "string" + } + } +} diff --git a/schemas/palomadex-pair-stable/raw/query.json b/schemas/palomadex-pair-stable/raw/query.json new file mode 100644 index 0000000..4bf998b --- /dev/null +++ b/schemas/palomadex-pair-stable/raw/query.json @@ -0,0 +1,303 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "QueryMsg", + "description": "This structure describes the query messages available in the contract.", + "oneOf": [ + { + "description": "Returns information about a pair in an object of type [`super::asset::PairInfo`].", + "type": "object", + "required": [ + "pair" + ], + "properties": { + "pair": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns information about a pool in an object of type [`PoolResponse`].", + "type": "object", + "required": [ + "pool" + ], + "properties": { + "pool": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns contract configuration settings in a custom [`ConfigResponse`] structure.", + "type": "object", + "required": [ + "config" + ], + "properties": { + "config": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns information about the share of the pool in a vector that contains objects of type [`Asset`].", + "type": "object", + "required": [ + "share" + ], + "properties": { + "share": { + "type": "object", + "required": [ + "amount" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns information about a swap simulation in a [`SimulationResponse`] object.", + "type": "object", + "required": [ + "simulation" + ], + "properties": { + "simulation": { + "type": "object", + "required": [ + "offer_asset" + ], + "properties": { + "ask_asset_info": { + "anyOf": [ + { + "$ref": "#/definitions/AssetInfo" + }, + { + "type": "null" + } + ] + }, + "offer_asset": { + "$ref": "#/definitions/Asset" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns information about cumulative prices in a [`ReverseSimulationResponse`] object.", + "type": "object", + "required": [ + "reverse_simulation" + ], + "properties": { + "reverse_simulation": { + "type": "object", + "required": [ + "ask_asset" + ], + "properties": { + "ask_asset": { + "$ref": "#/definitions/Asset" + }, + "offer_asset_info": { + "anyOf": [ + { + "$ref": "#/definitions/AssetInfo" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns information about the cumulative prices in a [`CumulativePricesResponse`] object", + "type": "object", + "required": [ + "cumulative_prices" + ], + "properties": { + "cumulative_prices": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns current D invariant in as a [`u128`] value", + "type": "object", + "required": [ + "query_compute_d" + ], + "properties": { + "query_compute_d": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns the balance of the specified asset that was in the pool just preceeding the moment of the specified block height creation.", + "type": "object", + "required": [ + "asset_balance_at" + ], + "properties": { + "asset_balance_at": { + "type": "object", + "required": [ + "asset_info", + "block_height" + ], + "properties": { + "asset_info": { + "$ref": "#/definitions/AssetInfo" + }, + "block_height": { + "$ref": "#/definitions/Uint64" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Query price from observations", + "type": "object", + "required": [ + "observe" + ], + "properties": { + "observe": { + "type": "object", + "required": [ + "seconds_ago" + ], + "properties": { + "seconds_ago": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "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" + }, + "Asset": { + "description": "This enum describes a Terra asset (native or CW20).", + "type": "object", + "required": [ + "amount", + "info" + ], + "properties": { + "amount": { + "description": "A token amount", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] + }, + "info": { + "description": "Information about an asset stored in a [`AssetInfo`] struct", + "allOf": [ + { + "$ref": "#/definitions/AssetInfo" + } + ] + } + }, + "additionalProperties": false + }, + "AssetInfo": { + "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use palomadex::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", + "oneOf": [ + { + "description": "Non-native Token", + "type": "object", + "required": [ + "token" + ], + "properties": { + "token": { + "type": "object", + "required": [ + "contract_addr" + ], + "properties": { + "contract_addr": { + "$ref": "#/definitions/Addr" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Native token", + "type": "object", + "required": [ + "native_token" + ], + "properties": { + "native_token": { + "type": "object", + "required": [ + "denom" + ], + "properties": { + "denom": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "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" + } + } +} diff --git a/schemas/palomadex-pair-stable/raw/response_to_asset_balance_at.json b/schemas/palomadex-pair-stable/raw/response_to_asset_balance_at.json new file mode 100644 index 0000000..2eaf6e9 --- /dev/null +++ b/schemas/palomadex-pair-stable/raw/response_to_asset_balance_at.json @@ -0,0 +1,18 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Nullable_Uint128", + "anyOf": [ + { + "$ref": "#/definitions/Uint128" + }, + { + "type": "null" + } + ], + "definitions": { + "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" + } + } +} diff --git a/schemas/palomadex-pair-stable/raw/response_to_config.json b/schemas/palomadex-pair-stable/raw/response_to_config.json new file mode 100644 index 0000000..ce805e9 --- /dev/null +++ b/schemas/palomadex-pair-stable/raw/response_to_config.json @@ -0,0 +1,57 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ConfigResponse", + "description": "This struct is used to return a query result with the general contract configuration.", + "type": "object", + "required": [ + "block_time_last", + "factory_addr", + "owner" + ], + "properties": { + "block_time_last": { + "description": "Last timestamp when the cumulative prices in the pool were updated", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "factory_addr": { + "description": "The factory contract address", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + }, + "owner": { + "description": "The contract owner", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + }, + "params": { + "description": "The pool's parameters", + "anyOf": [ + { + "$ref": "#/definitions/Binary" + }, + { + "type": "null" + } + ] + } + }, + "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" + }, + "Binary": { + "description": "Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec. See also .", + "type": "string" + } + } +} diff --git a/schemas/palomadex-pair-stable/raw/response_to_cumulative_prices.json b/schemas/palomadex-pair-stable/raw/response_to_cumulative_prices.json new file mode 100644 index 0000000..27f58d1 --- /dev/null +++ b/schemas/palomadex-pair-stable/raw/response_to_cumulative_prices.json @@ -0,0 +1,135 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "CumulativePricesResponse", + "description": "This structure is used to return a cumulative prices query response.", + "type": "object", + "required": [ + "assets", + "cumulative_prices", + "total_share" + ], + "properties": { + "assets": { + "description": "The assets in the pool to query", + "type": "array", + "items": { + "$ref": "#/definitions/Asset" + } + }, + "cumulative_prices": { + "description": "The vector contains cumulative prices for each pair of assets in the pool", + "type": "array", + "items": { + "type": "array", + "items": [ + { + "$ref": "#/definitions/AssetInfo" + }, + { + "$ref": "#/definitions/AssetInfo" + }, + { + "$ref": "#/definitions/Uint128" + } + ], + "maxItems": 3, + "minItems": 3 + } + }, + "total_share": { + "description": "The total amount of LP tokens currently issued", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] + } + }, + "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" + }, + "Asset": { + "description": "This enum describes a Terra asset (native or CW20).", + "type": "object", + "required": [ + "amount", + "info" + ], + "properties": { + "amount": { + "description": "A token amount", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] + }, + "info": { + "description": "Information about an asset stored in a [`AssetInfo`] struct", + "allOf": [ + { + "$ref": "#/definitions/AssetInfo" + } + ] + } + }, + "additionalProperties": false + }, + "AssetInfo": { + "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use palomadex::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", + "oneOf": [ + { + "description": "Non-native Token", + "type": "object", + "required": [ + "token" + ], + "properties": { + "token": { + "type": "object", + "required": [ + "contract_addr" + ], + "properties": { + "contract_addr": { + "$ref": "#/definitions/Addr" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Native token", + "type": "object", + "required": [ + "native_token" + ], + "properties": { + "native_token": { + "type": "object", + "required": [ + "denom" + ], + "properties": { + "denom": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "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" + } + } +} diff --git a/schemas/palomadex-pair-stable/raw/response_to_observe.json b/schemas/palomadex-pair-stable/raw/response_to_observe.json new file mode 100644 index 0000000..a8c389c --- /dev/null +++ b/schemas/palomadex-pair-stable/raw/response_to_observe.json @@ -0,0 +1,26 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "OracleObservation", + "type": "object", + "required": [ + "price", + "timestamp" + ], + "properties": { + "price": { + "$ref": "#/definitions/Decimal" + }, + "timestamp": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false, + "definitions": { + "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" + } + } +} diff --git a/schemas/palomadex-pair-stable/raw/response_to_pair.json b/schemas/palomadex-pair-stable/raw/response_to_pair.json new file mode 100644 index 0000000..bd4b3e5 --- /dev/null +++ b/schemas/palomadex-pair-stable/raw/response_to_pair.json @@ -0,0 +1,147 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "PairInfo", + "description": "This structure stores the main parameters for an palomadex pair", + "type": "object", + "required": [ + "asset_infos", + "contract_addr", + "liquidity_token", + "pair_type" + ], + "properties": { + "asset_infos": { + "description": "Asset information for the assets in the pool", + "type": "array", + "items": { + "$ref": "#/definitions/AssetInfo" + } + }, + "contract_addr": { + "description": "Pair contract address", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + }, + "liquidity_token": { + "description": "Pair LP token address", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + }, + "pair_type": { + "description": "The pool type (xyk, stableswap etc) available in [`PairType`]", + "allOf": [ + { + "$ref": "#/definitions/PairType" + } + ] + } + }, + "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" + }, + "AssetInfo": { + "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use palomadex::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", + "oneOf": [ + { + "description": "Non-native Token", + "type": "object", + "required": [ + "token" + ], + "properties": { + "token": { + "type": "object", + "required": [ + "contract_addr" + ], + "properties": { + "contract_addr": { + "$ref": "#/definitions/Addr" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Native token", + "type": "object", + "required": [ + "native_token" + ], + "properties": { + "native_token": { + "type": "object", + "required": [ + "denom" + ], + "properties": { + "denom": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "PairType": { + "description": "This enum describes available pair types. ## Available pool types ``` # use palomadex::factory::PairType::{Custom, Stable, Xyk}; Xyk {}; Stable {}; Custom(String::from(\"Custom\")); ```", + "oneOf": [ + { + "description": "XYK pair type", + "type": "object", + "required": [ + "xyk" + ], + "properties": { + "xyk": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Stable pair type", + "type": "object", + "required": [ + "stable" + ], + "properties": { + "stable": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Custom pair type", + "type": "object", + "required": [ + "custom" + ], + "properties": { + "custom": { + "type": "string" + } + }, + "additionalProperties": false + } + ] + } + } +} diff --git a/schemas/palomadex-pair-stable/raw/response_to_pool.json b/schemas/palomadex-pair-stable/raw/response_to_pool.json new file mode 100644 index 0000000..385810a --- /dev/null +++ b/schemas/palomadex-pair-stable/raw/response_to_pool.json @@ -0,0 +1,114 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "PoolResponse", + "description": "This struct is used to return a query result with the total amount of LP tokens and assets in a specific pool.", + "type": "object", + "required": [ + "assets", + "total_share" + ], + "properties": { + "assets": { + "description": "The assets in the pool together with asset amounts", + "type": "array", + "items": { + "$ref": "#/definitions/Asset" + } + }, + "total_share": { + "description": "The total amount of LP tokens currently issued", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] + } + }, + "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" + }, + "Asset": { + "description": "This enum describes a Terra asset (native or CW20).", + "type": "object", + "required": [ + "amount", + "info" + ], + "properties": { + "amount": { + "description": "A token amount", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] + }, + "info": { + "description": "Information about an asset stored in a [`AssetInfo`] struct", + "allOf": [ + { + "$ref": "#/definitions/AssetInfo" + } + ] + } + }, + "additionalProperties": false + }, + "AssetInfo": { + "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use palomadex::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", + "oneOf": [ + { + "description": "Non-native Token", + "type": "object", + "required": [ + "token" + ], + "properties": { + "token": { + "type": "object", + "required": [ + "contract_addr" + ], + "properties": { + "contract_addr": { + "$ref": "#/definitions/Addr" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Native token", + "type": "object", + "required": [ + "native_token" + ], + "properties": { + "native_token": { + "type": "object", + "required": [ + "denom" + ], + "properties": { + "denom": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "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" + } + } +} diff --git a/schemas/palomadex-pair-stable/raw/response_to_query_compute_d.json b/schemas/palomadex-pair-stable/raw/response_to_query_compute_d.json new file mode 100644 index 0000000..25b73e8 --- /dev/null +++ b/schemas/palomadex-pair-stable/raw/response_to_query_compute_d.json @@ -0,0 +1,6 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "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" +} diff --git a/schemas/palomadex-pair-stable/raw/response_to_reverse_simulation.json b/schemas/palomadex-pair-stable/raw/response_to_reverse_simulation.json new file mode 100644 index 0000000..ca711e1 --- /dev/null +++ b/schemas/palomadex-pair-stable/raw/response_to_reverse_simulation.json @@ -0,0 +1,44 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ReverseSimulationResponse", + "description": "This structure holds the parameters that are returned from a reverse swap simulation response.", + "type": "object", + "required": [ + "commission_amount", + "offer_amount", + "spread_amount" + ], + "properties": { + "commission_amount": { + "description": "The amount of fees charged by the transaction", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] + }, + "offer_amount": { + "description": "The amount of offer assets returned by the reverse swap", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] + }, + "spread_amount": { + "description": "The spread used in the swap operation", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] + } + }, + "additionalProperties": false, + "definitions": { + "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" + } + } +} diff --git a/schemas/palomadex-pair-stable/raw/response_to_share.json b/schemas/palomadex-pair-stable/raw/response_to_share.json new file mode 100644 index 0000000..e53d618 --- /dev/null +++ b/schemas/palomadex-pair-stable/raw/response_to_share.json @@ -0,0 +1,94 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Array_of_Asset", + "type": "array", + "items": { + "$ref": "#/definitions/Asset" + }, + "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" + }, + "Asset": { + "description": "This enum describes a Terra asset (native or CW20).", + "type": "object", + "required": [ + "amount", + "info" + ], + "properties": { + "amount": { + "description": "A token amount", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] + }, + "info": { + "description": "Information about an asset stored in a [`AssetInfo`] struct", + "allOf": [ + { + "$ref": "#/definitions/AssetInfo" + } + ] + } + }, + "additionalProperties": false + }, + "AssetInfo": { + "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use palomadex::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", + "oneOf": [ + { + "description": "Non-native Token", + "type": "object", + "required": [ + "token" + ], + "properties": { + "token": { + "type": "object", + "required": [ + "contract_addr" + ], + "properties": { + "contract_addr": { + "$ref": "#/definitions/Addr" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Native token", + "type": "object", + "required": [ + "native_token" + ], + "properties": { + "native_token": { + "type": "object", + "required": [ + "denom" + ], + "properties": { + "denom": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "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" + } + } +} diff --git a/schemas/palomadex-pair-stable/raw/response_to_simulation.json b/schemas/palomadex-pair-stable/raw/response_to_simulation.json new file mode 100644 index 0000000..4bc828d --- /dev/null +++ b/schemas/palomadex-pair-stable/raw/response_to_simulation.json @@ -0,0 +1,44 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "SimulationResponse", + "description": "This structure holds the parameters that are returned from a swap simulation response", + "type": "object", + "required": [ + "commission_amount", + "return_amount", + "spread_amount" + ], + "properties": { + "commission_amount": { + "description": "The amount of fees charged by the transaction", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] + }, + "return_amount": { + "description": "The amount of ask assets returned by the swap", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] + }, + "spread_amount": { + "description": "The spread used in the swap operation", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] + } + }, + "additionalProperties": false, + "definitions": { + "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" + } + } +} diff --git a/schemas/palomadex-pair/palomadex-pair.json b/schemas/palomadex-pair/palomadex-pair.json new file mode 100644 index 0000000..4e5eef4 --- /dev/null +++ b/schemas/palomadex-pair/palomadex-pair.json @@ -0,0 +1,1357 @@ +{ + "contract_name": "palomadex-pair", + "contract_version": "0.1.0", + "idl_version": "1.0.0", + "instantiate": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "InstantiateMsg", + "description": "This structure describes the parameters used for creating a contract.", + "type": "object", + "required": [ + "asset_infos", + "factory_addr", + "token_code_id" + ], + "properties": { + "asset_infos": { + "description": "Information about assets in the pool", + "type": "array", + "items": { + "$ref": "#/definitions/AssetInfo" + } + }, + "factory_addr": { + "description": "The factory contract address", + "type": "string" + }, + "init_params": { + "description": "Optional binary serialised parameters for custom pool types", + "anyOf": [ + { + "$ref": "#/definitions/Binary" + }, + { + "type": "null" + } + ] + }, + "token_code_id": { + "description": "The token contract code ID used for the tokens in the pool", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "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" + }, + "AssetInfo": { + "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use palomadex::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", + "oneOf": [ + { + "description": "Non-native Token", + "type": "object", + "required": [ + "token" + ], + "properties": { + "token": { + "type": "object", + "required": [ + "contract_addr" + ], + "properties": { + "contract_addr": { + "$ref": "#/definitions/Addr" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Native token", + "type": "object", + "required": [ + "native_token" + ], + "properties": { + "native_token": { + "type": "object", + "required": [ + "denom" + ], + "properties": { + "denom": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "Binary": { + "description": "Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec. See also .", + "type": "string" + } + } + }, + "execute": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ExecuteMsg", + "description": "This structure describes the execute messages available in the contract.", + "oneOf": [ + { + "description": "Receives a message of type [`Cw20ReceiveMsg`]", + "type": "object", + "required": [ + "receive" + ], + "properties": { + "receive": { + "$ref": "#/definitions/Cw20ReceiveMsg" + } + }, + "additionalProperties": false + }, + { + "description": "ProvideLiquidity allows someone to provide liquidity in the pool", + "type": "object", + "required": [ + "provide_liquidity" + ], + "properties": { + "provide_liquidity": { + "type": "object", + "required": [ + "assets" + ], + "properties": { + "assets": { + "description": "The assets available in the pool", + "type": "array", + "items": { + "$ref": "#/definitions/Asset" + } + }, + "receiver": { + "description": "The receiver of LP tokens", + "type": [ + "string", + "null" + ] + }, + "slippage_tolerance": { + "description": "The slippage tolerance that allows liquidity provision only if the price in the pool doesn't move too much", + "anyOf": [ + { + "$ref": "#/definitions/Decimal" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Swap performs a swap in the pool", + "type": "object", + "required": [ + "swap" + ], + "properties": { + "swap": { + "type": "object", + "required": [ + "offer_asset" + ], + "properties": { + "ask_asset_info": { + "anyOf": [ + { + "$ref": "#/definitions/AssetInfo" + }, + { + "type": "null" + } + ] + }, + "belief_price": { + "anyOf": [ + { + "$ref": "#/definitions/Decimal" + }, + { + "type": "null" + } + ] + }, + "max_spread": { + "anyOf": [ + { + "$ref": "#/definitions/Decimal" + }, + { + "type": "null" + } + ] + }, + "offer_asset": { + "$ref": "#/definitions/Asset" + }, + "to": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Update the pair configuration", + "type": "object", + "required": [ + "update_config" + ], + "properties": { + "update_config": { + "type": "object", + "required": [ + "params" + ], + "properties": { + "params": { + "$ref": "#/definitions/Binary" + } + }, + "additionalProperties": false + } + }, + "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" + }, + "Asset": { + "description": "This enum describes a Terra asset (native or CW20).", + "type": "object", + "required": [ + "amount", + "info" + ], + "properties": { + "amount": { + "description": "A token amount", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] + }, + "info": { + "description": "Information about an asset stored in a [`AssetInfo`] struct", + "allOf": [ + { + "$ref": "#/definitions/AssetInfo" + } + ] + } + }, + "additionalProperties": false + }, + "AssetInfo": { + "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use palomadex::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", + "oneOf": [ + { + "description": "Non-native Token", + "type": "object", + "required": [ + "token" + ], + "properties": { + "token": { + "type": "object", + "required": [ + "contract_addr" + ], + "properties": { + "contract_addr": { + "$ref": "#/definitions/Addr" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Native token", + "type": "object", + "required": [ + "native_token" + ], + "properties": { + "native_token": { + "type": "object", + "required": [ + "denom" + ], + "properties": { + "denom": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "Binary": { + "description": "Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec. See also .", + "type": "string" + }, + "Cw20ReceiveMsg": { + "description": "Cw20ReceiveMsg should be de/serialized under `Receive()` variant in a ExecuteMsg", + "type": "object", + "required": [ + "amount", + "msg", + "sender" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "msg": { + "$ref": "#/definitions/Binary" + }, + "sender": { + "type": "string" + } + }, + "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" + }, + "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" + } + } + }, + "query": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "QueryMsg", + "description": "This structure describes the query messages available in the contract.", + "oneOf": [ + { + "description": "Returns information about a pair in an object of type [`super::asset::PairInfo`].", + "type": "object", + "required": [ + "pair" + ], + "properties": { + "pair": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns information about a pool in an object of type [`PoolResponse`].", + "type": "object", + "required": [ + "pool" + ], + "properties": { + "pool": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns contract configuration settings in a custom [`ConfigResponse`] structure.", + "type": "object", + "required": [ + "config" + ], + "properties": { + "config": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns information about the share of the pool in a vector that contains objects of type [`Asset`].", + "type": "object", + "required": [ + "share" + ], + "properties": { + "share": { + "type": "object", + "required": [ + "amount" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns information about a swap simulation in a [`SimulationResponse`] object.", + "type": "object", + "required": [ + "simulation" + ], + "properties": { + "simulation": { + "type": "object", + "required": [ + "offer_asset" + ], + "properties": { + "ask_asset_info": { + "anyOf": [ + { + "$ref": "#/definitions/AssetInfo" + }, + { + "type": "null" + } + ] + }, + "offer_asset": { + "$ref": "#/definitions/Asset" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns information about cumulative prices in a [`ReverseSimulationResponse`] object.", + "type": "object", + "required": [ + "reverse_simulation" + ], + "properties": { + "reverse_simulation": { + "type": "object", + "required": [ + "ask_asset" + ], + "properties": { + "ask_asset": { + "$ref": "#/definitions/Asset" + }, + "offer_asset_info": { + "anyOf": [ + { + "$ref": "#/definitions/AssetInfo" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns information about the cumulative prices in a [`CumulativePricesResponse`] object", + "type": "object", + "required": [ + "cumulative_prices" + ], + "properties": { + "cumulative_prices": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns current D invariant in as a [`u128`] value", + "type": "object", + "required": [ + "query_compute_d" + ], + "properties": { + "query_compute_d": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns the balance of the specified asset that was in the pool just preceeding the moment of the specified block height creation.", + "type": "object", + "required": [ + "asset_balance_at" + ], + "properties": { + "asset_balance_at": { + "type": "object", + "required": [ + "asset_info", + "block_height" + ], + "properties": { + "asset_info": { + "$ref": "#/definitions/AssetInfo" + }, + "block_height": { + "$ref": "#/definitions/Uint64" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Query price from observations", + "type": "object", + "required": [ + "observe" + ], + "properties": { + "observe": { + "type": "object", + "required": [ + "seconds_ago" + ], + "properties": { + "seconds_ago": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "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" + }, + "Asset": { + "description": "This enum describes a Terra asset (native or CW20).", + "type": "object", + "required": [ + "amount", + "info" + ], + "properties": { + "amount": { + "description": "A token amount", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] + }, + "info": { + "description": "Information about an asset stored in a [`AssetInfo`] struct", + "allOf": [ + { + "$ref": "#/definitions/AssetInfo" + } + ] + } + }, + "additionalProperties": false + }, + "AssetInfo": { + "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use palomadex::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", + "oneOf": [ + { + "description": "Non-native Token", + "type": "object", + "required": [ + "token" + ], + "properties": { + "token": { + "type": "object", + "required": [ + "contract_addr" + ], + "properties": { + "contract_addr": { + "$ref": "#/definitions/Addr" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Native token", + "type": "object", + "required": [ + "native_token" + ], + "properties": { + "native_token": { + "type": "object", + "required": [ + "denom" + ], + "properties": { + "denom": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "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" + } + } + }, + "migrate": null, + "sudo": null, + "responses": { + "asset_balance_at": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Nullable_Uint128", + "anyOf": [ + { + "$ref": "#/definitions/Uint128" + }, + { + "type": "null" + } + ], + "definitions": { + "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" + } + } + }, + "config": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ConfigResponse", + "description": "This struct is used to return a query result with the general contract configuration.", + "type": "object", + "required": [ + "block_time_last", + "factory_addr", + "owner" + ], + "properties": { + "block_time_last": { + "description": "Last timestamp when the cumulative prices in the pool were updated", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "factory_addr": { + "description": "The factory contract address", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + }, + "owner": { + "description": "The contract owner", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + }, + "params": { + "description": "The pool's parameters", + "anyOf": [ + { + "$ref": "#/definitions/Binary" + }, + { + "type": "null" + } + ] + } + }, + "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" + }, + "Binary": { + "description": "Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec. See also .", + "type": "string" + } + } + }, + "cumulative_prices": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "CumulativePricesResponse", + "description": "This structure is used to return a cumulative prices query response.", + "type": "object", + "required": [ + "assets", + "cumulative_prices", + "total_share" + ], + "properties": { + "assets": { + "description": "The assets in the pool to query", + "type": "array", + "items": { + "$ref": "#/definitions/Asset" + } + }, + "cumulative_prices": { + "description": "The vector contains cumulative prices for each pair of assets in the pool", + "type": "array", + "items": { + "type": "array", + "items": [ + { + "$ref": "#/definitions/AssetInfo" + }, + { + "$ref": "#/definitions/AssetInfo" + }, + { + "$ref": "#/definitions/Uint128" + } + ], + "maxItems": 3, + "minItems": 3 + } + }, + "total_share": { + "description": "The total amount of LP tokens currently issued", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] + } + }, + "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" + }, + "Asset": { + "description": "This enum describes a Terra asset (native or CW20).", + "type": "object", + "required": [ + "amount", + "info" + ], + "properties": { + "amount": { + "description": "A token amount", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] + }, + "info": { + "description": "Information about an asset stored in a [`AssetInfo`] struct", + "allOf": [ + { + "$ref": "#/definitions/AssetInfo" + } + ] + } + }, + "additionalProperties": false + }, + "AssetInfo": { + "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use palomadex::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", + "oneOf": [ + { + "description": "Non-native Token", + "type": "object", + "required": [ + "token" + ], + "properties": { + "token": { + "type": "object", + "required": [ + "contract_addr" + ], + "properties": { + "contract_addr": { + "$ref": "#/definitions/Addr" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Native token", + "type": "object", + "required": [ + "native_token" + ], + "properties": { + "native_token": { + "type": "object", + "required": [ + "denom" + ], + "properties": { + "denom": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "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" + } + } + }, + "observe": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "OracleObservation", + "type": "object", + "required": [ + "price", + "timestamp" + ], + "properties": { + "price": { + "$ref": "#/definitions/Decimal" + }, + "timestamp": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false, + "definitions": { + "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" + } + } + }, + "pair": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "PairInfo", + "description": "This structure stores the main parameters for an palomadex pair", + "type": "object", + "required": [ + "asset_infos", + "contract_addr", + "liquidity_token", + "pair_type" + ], + "properties": { + "asset_infos": { + "description": "Asset information for the assets in the pool", + "type": "array", + "items": { + "$ref": "#/definitions/AssetInfo" + } + }, + "contract_addr": { + "description": "Pair contract address", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + }, + "liquidity_token": { + "description": "Pair LP token address", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + }, + "pair_type": { + "description": "The pool type (xyk, stableswap etc) available in [`PairType`]", + "allOf": [ + { + "$ref": "#/definitions/PairType" + } + ] + } + }, + "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" + }, + "AssetInfo": { + "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use palomadex::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", + "oneOf": [ + { + "description": "Non-native Token", + "type": "object", + "required": [ + "token" + ], + "properties": { + "token": { + "type": "object", + "required": [ + "contract_addr" + ], + "properties": { + "contract_addr": { + "$ref": "#/definitions/Addr" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Native token", + "type": "object", + "required": [ + "native_token" + ], + "properties": { + "native_token": { + "type": "object", + "required": [ + "denom" + ], + "properties": { + "denom": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "PairType": { + "description": "This enum describes available pair types. ## Available pool types ``` # use palomadex::factory::PairType::{Custom, Stable, Xyk}; Xyk {}; Stable {}; Custom(String::from(\"Custom\")); ```", + "oneOf": [ + { + "description": "XYK pair type", + "type": "object", + "required": [ + "xyk" + ], + "properties": { + "xyk": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Stable pair type", + "type": "object", + "required": [ + "stable" + ], + "properties": { + "stable": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Custom pair type", + "type": "object", + "required": [ + "custom" + ], + "properties": { + "custom": { + "type": "string" + } + }, + "additionalProperties": false + } + ] + } + } + }, + "pool": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "PoolResponse", + "description": "This struct is used to return a query result with the total amount of LP tokens and assets in a specific pool.", + "type": "object", + "required": [ + "assets", + "total_share" + ], + "properties": { + "assets": { + "description": "The assets in the pool together with asset amounts", + "type": "array", + "items": { + "$ref": "#/definitions/Asset" + } + }, + "total_share": { + "description": "The total amount of LP tokens currently issued", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] + } + }, + "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" + }, + "Asset": { + "description": "This enum describes a Terra asset (native or CW20).", + "type": "object", + "required": [ + "amount", + "info" + ], + "properties": { + "amount": { + "description": "A token amount", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] + }, + "info": { + "description": "Information about an asset stored in a [`AssetInfo`] struct", + "allOf": [ + { + "$ref": "#/definitions/AssetInfo" + } + ] + } + }, + "additionalProperties": false + }, + "AssetInfo": { + "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use palomadex::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", + "oneOf": [ + { + "description": "Non-native Token", + "type": "object", + "required": [ + "token" + ], + "properties": { + "token": { + "type": "object", + "required": [ + "contract_addr" + ], + "properties": { + "contract_addr": { + "$ref": "#/definitions/Addr" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Native token", + "type": "object", + "required": [ + "native_token" + ], + "properties": { + "native_token": { + "type": "object", + "required": [ + "denom" + ], + "properties": { + "denom": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "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" + } + } + }, + "query_compute_d": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "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" + }, + "reverse_simulation": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ReverseSimulationResponse", + "description": "This structure holds the parameters that are returned from a reverse swap simulation response.", + "type": "object", + "required": [ + "commission_amount", + "offer_amount", + "spread_amount" + ], + "properties": { + "commission_amount": { + "description": "The amount of fees charged by the transaction", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] + }, + "offer_amount": { + "description": "The amount of offer assets returned by the reverse swap", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] + }, + "spread_amount": { + "description": "The spread used in the swap operation", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] + } + }, + "additionalProperties": false, + "definitions": { + "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" + } + } + }, + "share": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Array_of_Asset", + "type": "array", + "items": { + "$ref": "#/definitions/Asset" + }, + "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" + }, + "Asset": { + "description": "This enum describes a Terra asset (native or CW20).", + "type": "object", + "required": [ + "amount", + "info" + ], + "properties": { + "amount": { + "description": "A token amount", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] + }, + "info": { + "description": "Information about an asset stored in a [`AssetInfo`] struct", + "allOf": [ + { + "$ref": "#/definitions/AssetInfo" + } + ] + } + }, + "additionalProperties": false + }, + "AssetInfo": { + "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use palomadex::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", + "oneOf": [ + { + "description": "Non-native Token", + "type": "object", + "required": [ + "token" + ], + "properties": { + "token": { + "type": "object", + "required": [ + "contract_addr" + ], + "properties": { + "contract_addr": { + "$ref": "#/definitions/Addr" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Native token", + "type": "object", + "required": [ + "native_token" + ], + "properties": { + "native_token": { + "type": "object", + "required": [ + "denom" + ], + "properties": { + "denom": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "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" + } + } + }, + "simulation": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "SimulationResponse", + "description": "This structure holds the parameters that are returned from a swap simulation response", + "type": "object", + "required": [ + "commission_amount", + "return_amount", + "spread_amount" + ], + "properties": { + "commission_amount": { + "description": "The amount of fees charged by the transaction", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] + }, + "return_amount": { + "description": "The amount of ask assets returned by the swap", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] + }, + "spread_amount": { + "description": "The spread used in the swap operation", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] + } + }, + "additionalProperties": false, + "definitions": { + "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" + } + } + } + } +} diff --git a/schemas/palomadex-pair/raw/execute.json b/schemas/palomadex-pair/raw/execute.json new file mode 100644 index 0000000..0cd254e --- /dev/null +++ b/schemas/palomadex-pair/raw/execute.json @@ -0,0 +1,259 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ExecuteMsg", + "description": "This structure describes the execute messages available in the contract.", + "oneOf": [ + { + "description": "Receives a message of type [`Cw20ReceiveMsg`]", + "type": "object", + "required": [ + "receive" + ], + "properties": { + "receive": { + "$ref": "#/definitions/Cw20ReceiveMsg" + } + }, + "additionalProperties": false + }, + { + "description": "ProvideLiquidity allows someone to provide liquidity in the pool", + "type": "object", + "required": [ + "provide_liquidity" + ], + "properties": { + "provide_liquidity": { + "type": "object", + "required": [ + "assets" + ], + "properties": { + "assets": { + "description": "The assets available in the pool", + "type": "array", + "items": { + "$ref": "#/definitions/Asset" + } + }, + "receiver": { + "description": "The receiver of LP tokens", + "type": [ + "string", + "null" + ] + }, + "slippage_tolerance": { + "description": "The slippage tolerance that allows liquidity provision only if the price in the pool doesn't move too much", + "anyOf": [ + { + "$ref": "#/definitions/Decimal" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Swap performs a swap in the pool", + "type": "object", + "required": [ + "swap" + ], + "properties": { + "swap": { + "type": "object", + "required": [ + "offer_asset" + ], + "properties": { + "ask_asset_info": { + "anyOf": [ + { + "$ref": "#/definitions/AssetInfo" + }, + { + "type": "null" + } + ] + }, + "belief_price": { + "anyOf": [ + { + "$ref": "#/definitions/Decimal" + }, + { + "type": "null" + } + ] + }, + "max_spread": { + "anyOf": [ + { + "$ref": "#/definitions/Decimal" + }, + { + "type": "null" + } + ] + }, + "offer_asset": { + "$ref": "#/definitions/Asset" + }, + "to": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Update the pair configuration", + "type": "object", + "required": [ + "update_config" + ], + "properties": { + "update_config": { + "type": "object", + "required": [ + "params" + ], + "properties": { + "params": { + "$ref": "#/definitions/Binary" + } + }, + "additionalProperties": false + } + }, + "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" + }, + "Asset": { + "description": "This enum describes a Terra asset (native or CW20).", + "type": "object", + "required": [ + "amount", + "info" + ], + "properties": { + "amount": { + "description": "A token amount", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] + }, + "info": { + "description": "Information about an asset stored in a [`AssetInfo`] struct", + "allOf": [ + { + "$ref": "#/definitions/AssetInfo" + } + ] + } + }, + "additionalProperties": false + }, + "AssetInfo": { + "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use palomadex::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", + "oneOf": [ + { + "description": "Non-native Token", + "type": "object", + "required": [ + "token" + ], + "properties": { + "token": { + "type": "object", + "required": [ + "contract_addr" + ], + "properties": { + "contract_addr": { + "$ref": "#/definitions/Addr" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Native token", + "type": "object", + "required": [ + "native_token" + ], + "properties": { + "native_token": { + "type": "object", + "required": [ + "denom" + ], + "properties": { + "denom": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "Binary": { + "description": "Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec. See also .", + "type": "string" + }, + "Cw20ReceiveMsg": { + "description": "Cw20ReceiveMsg should be de/serialized under `Receive()` variant in a ExecuteMsg", + "type": "object", + "required": [ + "amount", + "msg", + "sender" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "msg": { + "$ref": "#/definitions/Binary" + }, + "sender": { + "type": "string" + } + }, + "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" + }, + "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" + } + } +} diff --git a/schemas/palomadex-pair/raw/instantiate.json b/schemas/palomadex-pair/raw/instantiate.json new file mode 100644 index 0000000..4782a48 --- /dev/null +++ b/schemas/palomadex-pair/raw/instantiate.json @@ -0,0 +1,101 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "InstantiateMsg", + "description": "This structure describes the parameters used for creating a contract.", + "type": "object", + "required": [ + "asset_infos", + "factory_addr", + "token_code_id" + ], + "properties": { + "asset_infos": { + "description": "Information about assets in the pool", + "type": "array", + "items": { + "$ref": "#/definitions/AssetInfo" + } + }, + "factory_addr": { + "description": "The factory contract address", + "type": "string" + }, + "init_params": { + "description": "Optional binary serialised parameters for custom pool types", + "anyOf": [ + { + "$ref": "#/definitions/Binary" + }, + { + "type": "null" + } + ] + }, + "token_code_id": { + "description": "The token contract code ID used for the tokens in the pool", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "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" + }, + "AssetInfo": { + "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use palomadex::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", + "oneOf": [ + { + "description": "Non-native Token", + "type": "object", + "required": [ + "token" + ], + "properties": { + "token": { + "type": "object", + "required": [ + "contract_addr" + ], + "properties": { + "contract_addr": { + "$ref": "#/definitions/Addr" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Native token", + "type": "object", + "required": [ + "native_token" + ], + "properties": { + "native_token": { + "type": "object", + "required": [ + "denom" + ], + "properties": { + "denom": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "Binary": { + "description": "Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec. See also .", + "type": "string" + } + } +} diff --git a/schemas/palomadex-pair/raw/query.json b/schemas/palomadex-pair/raw/query.json new file mode 100644 index 0000000..4bf998b --- /dev/null +++ b/schemas/palomadex-pair/raw/query.json @@ -0,0 +1,303 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "QueryMsg", + "description": "This structure describes the query messages available in the contract.", + "oneOf": [ + { + "description": "Returns information about a pair in an object of type [`super::asset::PairInfo`].", + "type": "object", + "required": [ + "pair" + ], + "properties": { + "pair": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns information about a pool in an object of type [`PoolResponse`].", + "type": "object", + "required": [ + "pool" + ], + "properties": { + "pool": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns contract configuration settings in a custom [`ConfigResponse`] structure.", + "type": "object", + "required": [ + "config" + ], + "properties": { + "config": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns information about the share of the pool in a vector that contains objects of type [`Asset`].", + "type": "object", + "required": [ + "share" + ], + "properties": { + "share": { + "type": "object", + "required": [ + "amount" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns information about a swap simulation in a [`SimulationResponse`] object.", + "type": "object", + "required": [ + "simulation" + ], + "properties": { + "simulation": { + "type": "object", + "required": [ + "offer_asset" + ], + "properties": { + "ask_asset_info": { + "anyOf": [ + { + "$ref": "#/definitions/AssetInfo" + }, + { + "type": "null" + } + ] + }, + "offer_asset": { + "$ref": "#/definitions/Asset" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns information about cumulative prices in a [`ReverseSimulationResponse`] object.", + "type": "object", + "required": [ + "reverse_simulation" + ], + "properties": { + "reverse_simulation": { + "type": "object", + "required": [ + "ask_asset" + ], + "properties": { + "ask_asset": { + "$ref": "#/definitions/Asset" + }, + "offer_asset_info": { + "anyOf": [ + { + "$ref": "#/definitions/AssetInfo" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns information about the cumulative prices in a [`CumulativePricesResponse`] object", + "type": "object", + "required": [ + "cumulative_prices" + ], + "properties": { + "cumulative_prices": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns current D invariant in as a [`u128`] value", + "type": "object", + "required": [ + "query_compute_d" + ], + "properties": { + "query_compute_d": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns the balance of the specified asset that was in the pool just preceeding the moment of the specified block height creation.", + "type": "object", + "required": [ + "asset_balance_at" + ], + "properties": { + "asset_balance_at": { + "type": "object", + "required": [ + "asset_info", + "block_height" + ], + "properties": { + "asset_info": { + "$ref": "#/definitions/AssetInfo" + }, + "block_height": { + "$ref": "#/definitions/Uint64" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Query price from observations", + "type": "object", + "required": [ + "observe" + ], + "properties": { + "observe": { + "type": "object", + "required": [ + "seconds_ago" + ], + "properties": { + "seconds_ago": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "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" + }, + "Asset": { + "description": "This enum describes a Terra asset (native or CW20).", + "type": "object", + "required": [ + "amount", + "info" + ], + "properties": { + "amount": { + "description": "A token amount", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] + }, + "info": { + "description": "Information about an asset stored in a [`AssetInfo`] struct", + "allOf": [ + { + "$ref": "#/definitions/AssetInfo" + } + ] + } + }, + "additionalProperties": false + }, + "AssetInfo": { + "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use palomadex::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", + "oneOf": [ + { + "description": "Non-native Token", + "type": "object", + "required": [ + "token" + ], + "properties": { + "token": { + "type": "object", + "required": [ + "contract_addr" + ], + "properties": { + "contract_addr": { + "$ref": "#/definitions/Addr" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Native token", + "type": "object", + "required": [ + "native_token" + ], + "properties": { + "native_token": { + "type": "object", + "required": [ + "denom" + ], + "properties": { + "denom": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "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" + } + } +} diff --git a/schemas/palomadex-pair/raw/response_to_asset_balance_at.json b/schemas/palomadex-pair/raw/response_to_asset_balance_at.json new file mode 100644 index 0000000..2eaf6e9 --- /dev/null +++ b/schemas/palomadex-pair/raw/response_to_asset_balance_at.json @@ -0,0 +1,18 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Nullable_Uint128", + "anyOf": [ + { + "$ref": "#/definitions/Uint128" + }, + { + "type": "null" + } + ], + "definitions": { + "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" + } + } +} diff --git a/schemas/palomadex-pair/raw/response_to_config.json b/schemas/palomadex-pair/raw/response_to_config.json new file mode 100644 index 0000000..ce805e9 --- /dev/null +++ b/schemas/palomadex-pair/raw/response_to_config.json @@ -0,0 +1,57 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ConfigResponse", + "description": "This struct is used to return a query result with the general contract configuration.", + "type": "object", + "required": [ + "block_time_last", + "factory_addr", + "owner" + ], + "properties": { + "block_time_last": { + "description": "Last timestamp when the cumulative prices in the pool were updated", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "factory_addr": { + "description": "The factory contract address", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + }, + "owner": { + "description": "The contract owner", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + }, + "params": { + "description": "The pool's parameters", + "anyOf": [ + { + "$ref": "#/definitions/Binary" + }, + { + "type": "null" + } + ] + } + }, + "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" + }, + "Binary": { + "description": "Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec. See also .", + "type": "string" + } + } +} diff --git a/schemas/palomadex-pair/raw/response_to_cumulative_prices.json b/schemas/palomadex-pair/raw/response_to_cumulative_prices.json new file mode 100644 index 0000000..27f58d1 --- /dev/null +++ b/schemas/palomadex-pair/raw/response_to_cumulative_prices.json @@ -0,0 +1,135 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "CumulativePricesResponse", + "description": "This structure is used to return a cumulative prices query response.", + "type": "object", + "required": [ + "assets", + "cumulative_prices", + "total_share" + ], + "properties": { + "assets": { + "description": "The assets in the pool to query", + "type": "array", + "items": { + "$ref": "#/definitions/Asset" + } + }, + "cumulative_prices": { + "description": "The vector contains cumulative prices for each pair of assets in the pool", + "type": "array", + "items": { + "type": "array", + "items": [ + { + "$ref": "#/definitions/AssetInfo" + }, + { + "$ref": "#/definitions/AssetInfo" + }, + { + "$ref": "#/definitions/Uint128" + } + ], + "maxItems": 3, + "minItems": 3 + } + }, + "total_share": { + "description": "The total amount of LP tokens currently issued", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] + } + }, + "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" + }, + "Asset": { + "description": "This enum describes a Terra asset (native or CW20).", + "type": "object", + "required": [ + "amount", + "info" + ], + "properties": { + "amount": { + "description": "A token amount", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] + }, + "info": { + "description": "Information about an asset stored in a [`AssetInfo`] struct", + "allOf": [ + { + "$ref": "#/definitions/AssetInfo" + } + ] + } + }, + "additionalProperties": false + }, + "AssetInfo": { + "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use palomadex::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", + "oneOf": [ + { + "description": "Non-native Token", + "type": "object", + "required": [ + "token" + ], + "properties": { + "token": { + "type": "object", + "required": [ + "contract_addr" + ], + "properties": { + "contract_addr": { + "$ref": "#/definitions/Addr" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Native token", + "type": "object", + "required": [ + "native_token" + ], + "properties": { + "native_token": { + "type": "object", + "required": [ + "denom" + ], + "properties": { + "denom": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "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" + } + } +} diff --git a/schemas/palomadex-pair/raw/response_to_observe.json b/schemas/palomadex-pair/raw/response_to_observe.json new file mode 100644 index 0000000..a8c389c --- /dev/null +++ b/schemas/palomadex-pair/raw/response_to_observe.json @@ -0,0 +1,26 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "OracleObservation", + "type": "object", + "required": [ + "price", + "timestamp" + ], + "properties": { + "price": { + "$ref": "#/definitions/Decimal" + }, + "timestamp": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false, + "definitions": { + "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" + } + } +} diff --git a/schemas/palomadex-pair/raw/response_to_pair.json b/schemas/palomadex-pair/raw/response_to_pair.json new file mode 100644 index 0000000..bd4b3e5 --- /dev/null +++ b/schemas/palomadex-pair/raw/response_to_pair.json @@ -0,0 +1,147 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "PairInfo", + "description": "This structure stores the main parameters for an palomadex pair", + "type": "object", + "required": [ + "asset_infos", + "contract_addr", + "liquidity_token", + "pair_type" + ], + "properties": { + "asset_infos": { + "description": "Asset information for the assets in the pool", + "type": "array", + "items": { + "$ref": "#/definitions/AssetInfo" + } + }, + "contract_addr": { + "description": "Pair contract address", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + }, + "liquidity_token": { + "description": "Pair LP token address", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + }, + "pair_type": { + "description": "The pool type (xyk, stableswap etc) available in [`PairType`]", + "allOf": [ + { + "$ref": "#/definitions/PairType" + } + ] + } + }, + "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" + }, + "AssetInfo": { + "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use palomadex::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", + "oneOf": [ + { + "description": "Non-native Token", + "type": "object", + "required": [ + "token" + ], + "properties": { + "token": { + "type": "object", + "required": [ + "contract_addr" + ], + "properties": { + "contract_addr": { + "$ref": "#/definitions/Addr" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Native token", + "type": "object", + "required": [ + "native_token" + ], + "properties": { + "native_token": { + "type": "object", + "required": [ + "denom" + ], + "properties": { + "denom": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "PairType": { + "description": "This enum describes available pair types. ## Available pool types ``` # use palomadex::factory::PairType::{Custom, Stable, Xyk}; Xyk {}; Stable {}; Custom(String::from(\"Custom\")); ```", + "oneOf": [ + { + "description": "XYK pair type", + "type": "object", + "required": [ + "xyk" + ], + "properties": { + "xyk": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Stable pair type", + "type": "object", + "required": [ + "stable" + ], + "properties": { + "stable": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Custom pair type", + "type": "object", + "required": [ + "custom" + ], + "properties": { + "custom": { + "type": "string" + } + }, + "additionalProperties": false + } + ] + } + } +} diff --git a/schemas/palomadex-pair/raw/response_to_pool.json b/schemas/palomadex-pair/raw/response_to_pool.json new file mode 100644 index 0000000..385810a --- /dev/null +++ b/schemas/palomadex-pair/raw/response_to_pool.json @@ -0,0 +1,114 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "PoolResponse", + "description": "This struct is used to return a query result with the total amount of LP tokens and assets in a specific pool.", + "type": "object", + "required": [ + "assets", + "total_share" + ], + "properties": { + "assets": { + "description": "The assets in the pool together with asset amounts", + "type": "array", + "items": { + "$ref": "#/definitions/Asset" + } + }, + "total_share": { + "description": "The total amount of LP tokens currently issued", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] + } + }, + "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" + }, + "Asset": { + "description": "This enum describes a Terra asset (native or CW20).", + "type": "object", + "required": [ + "amount", + "info" + ], + "properties": { + "amount": { + "description": "A token amount", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] + }, + "info": { + "description": "Information about an asset stored in a [`AssetInfo`] struct", + "allOf": [ + { + "$ref": "#/definitions/AssetInfo" + } + ] + } + }, + "additionalProperties": false + }, + "AssetInfo": { + "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use palomadex::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", + "oneOf": [ + { + "description": "Non-native Token", + "type": "object", + "required": [ + "token" + ], + "properties": { + "token": { + "type": "object", + "required": [ + "contract_addr" + ], + "properties": { + "contract_addr": { + "$ref": "#/definitions/Addr" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Native token", + "type": "object", + "required": [ + "native_token" + ], + "properties": { + "native_token": { + "type": "object", + "required": [ + "denom" + ], + "properties": { + "denom": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "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" + } + } +} diff --git a/schemas/palomadex-pair/raw/response_to_query_compute_d.json b/schemas/palomadex-pair/raw/response_to_query_compute_d.json new file mode 100644 index 0000000..25b73e8 --- /dev/null +++ b/schemas/palomadex-pair/raw/response_to_query_compute_d.json @@ -0,0 +1,6 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "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" +} diff --git a/schemas/palomadex-pair/raw/response_to_reverse_simulation.json b/schemas/palomadex-pair/raw/response_to_reverse_simulation.json new file mode 100644 index 0000000..ca711e1 --- /dev/null +++ b/schemas/palomadex-pair/raw/response_to_reverse_simulation.json @@ -0,0 +1,44 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ReverseSimulationResponse", + "description": "This structure holds the parameters that are returned from a reverse swap simulation response.", + "type": "object", + "required": [ + "commission_amount", + "offer_amount", + "spread_amount" + ], + "properties": { + "commission_amount": { + "description": "The amount of fees charged by the transaction", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] + }, + "offer_amount": { + "description": "The amount of offer assets returned by the reverse swap", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] + }, + "spread_amount": { + "description": "The spread used in the swap operation", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] + } + }, + "additionalProperties": false, + "definitions": { + "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" + } + } +} diff --git a/schemas/palomadex-pair/raw/response_to_share.json b/schemas/palomadex-pair/raw/response_to_share.json new file mode 100644 index 0000000..e53d618 --- /dev/null +++ b/schemas/palomadex-pair/raw/response_to_share.json @@ -0,0 +1,94 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Array_of_Asset", + "type": "array", + "items": { + "$ref": "#/definitions/Asset" + }, + "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" + }, + "Asset": { + "description": "This enum describes a Terra asset (native or CW20).", + "type": "object", + "required": [ + "amount", + "info" + ], + "properties": { + "amount": { + "description": "A token amount", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] + }, + "info": { + "description": "Information about an asset stored in a [`AssetInfo`] struct", + "allOf": [ + { + "$ref": "#/definitions/AssetInfo" + } + ] + } + }, + "additionalProperties": false + }, + "AssetInfo": { + "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use palomadex::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", + "oneOf": [ + { + "description": "Non-native Token", + "type": "object", + "required": [ + "token" + ], + "properties": { + "token": { + "type": "object", + "required": [ + "contract_addr" + ], + "properties": { + "contract_addr": { + "$ref": "#/definitions/Addr" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Native token", + "type": "object", + "required": [ + "native_token" + ], + "properties": { + "native_token": { + "type": "object", + "required": [ + "denom" + ], + "properties": { + "denom": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "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" + } + } +} diff --git a/schemas/palomadex-pair/raw/response_to_simulation.json b/schemas/palomadex-pair/raw/response_to_simulation.json new file mode 100644 index 0000000..4bc828d --- /dev/null +++ b/schemas/palomadex-pair/raw/response_to_simulation.json @@ -0,0 +1,44 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "SimulationResponse", + "description": "This structure holds the parameters that are returned from a swap simulation response", + "type": "object", + "required": [ + "commission_amount", + "return_amount", + "spread_amount" + ], + "properties": { + "commission_amount": { + "description": "The amount of fees charged by the transaction", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] + }, + "return_amount": { + "description": "The amount of ask assets returned by the swap", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] + }, + "spread_amount": { + "description": "The spread used in the swap operation", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] + } + }, + "additionalProperties": false, + "definitions": { + "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" + } + } +} diff --git a/schemas/palomadex-router/palomadex-router.json b/schemas/palomadex-router/palomadex-router.json new file mode 100644 index 0000000..93e6569 --- /dev/null +++ b/schemas/palomadex-router/palomadex-router.json @@ -0,0 +1,521 @@ +{ + "contract_name": "palomadex-router", + "contract_version": "0.1.0", + "idl_version": "1.0.0", + "instantiate": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "InstantiateMsg", + "description": "This structure holds the parameters used for creating a contract.", + "type": "object", + "required": [ + "palomadex_factory" + ], + "properties": { + "palomadex_factory": { + "description": "The palomadex factory contract address", + "type": "string" + } + }, + "additionalProperties": false + }, + "execute": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ExecuteMsg", + "description": "This structure describes the execute messages available in the contract.", + "oneOf": [ + { + "description": "Receive receives a message of type [`Cw20ReceiveMsg`] and processes it depending on the received template", + "type": "object", + "required": [ + "receive" + ], + "properties": { + "receive": { + "$ref": "#/definitions/Cw20ReceiveMsg" + } + }, + "additionalProperties": false + }, + { + "description": "ExecuteSwapOperations processes multiple swaps while mentioning the minimum amount of tokens to receive for the last swap operation", + "type": "object", + "required": [ + "execute_swap_operations" + ], + "properties": { + "execute_swap_operations": { + "type": "object", + "required": [ + "operations" + ], + "properties": { + "max_spread": { + "anyOf": [ + { + "$ref": "#/definitions/Decimal" + }, + { + "type": "null" + } + ] + }, + "minimum_receive": { + "anyOf": [ + { + "$ref": "#/definitions/Uint128" + }, + { + "type": "null" + } + ] + }, + "operations": { + "type": "array", + "items": { + "$ref": "#/definitions/SwapOperation" + } + }, + "to": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Internal use ExecuteSwapOperation executes a single swap operation", + "type": "object", + "required": [ + "execute_swap_operation" + ], + "properties": { + "execute_swap_operation": { + "type": "object", + "required": [ + "operation", + "single" + ], + "properties": { + "max_spread": { + "anyOf": [ + { + "$ref": "#/definitions/Decimal" + }, + { + "type": "null" + } + ] + }, + "operation": { + "$ref": "#/definitions/SwapOperation" + }, + "single": { + "type": "boolean" + }, + "to": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + } + }, + "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" + }, + "AssetInfo": { + "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use palomadex::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", + "oneOf": [ + { + "description": "Non-native Token", + "type": "object", + "required": [ + "token" + ], + "properties": { + "token": { + "type": "object", + "required": [ + "contract_addr" + ], + "properties": { + "contract_addr": { + "$ref": "#/definitions/Addr" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Native token", + "type": "object", + "required": [ + "native_token" + ], + "properties": { + "native_token": { + "type": "object", + "required": [ + "denom" + ], + "properties": { + "denom": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "Binary": { + "description": "Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec. See also .", + "type": "string" + }, + "Cw20ReceiveMsg": { + "description": "Cw20ReceiveMsg should be de/serialized under `Receive()` variant in a ExecuteMsg", + "type": "object", + "required": [ + "amount", + "msg", + "sender" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "msg": { + "$ref": "#/definitions/Binary" + }, + "sender": { + "type": "string" + } + }, + "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" + }, + "SwapOperation": { + "description": "This enum describes a swap operation.", + "oneOf": [ + { + "description": "Native swap", + "type": "object", + "required": [ + "native_swap" + ], + "properties": { + "native_swap": { + "type": "object", + "required": [ + "ask_denom", + "offer_denom" + ], + "properties": { + "ask_denom": { + "description": "The name (denomination) of the native asset to swap to", + "type": "string" + }, + "offer_denom": { + "description": "The name (denomination) of the native asset to swap from", + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "ASTRO swap", + "type": "object", + "required": [ + "astro_swap" + ], + "properties": { + "astro_swap": { + "type": "object", + "required": [ + "ask_asset_info", + "offer_asset_info" + ], + "properties": { + "ask_asset_info": { + "description": "Information about the asset we swap to", + "allOf": [ + { + "$ref": "#/definitions/AssetInfo" + } + ] + }, + "offer_asset_info": { + "description": "Information about the asset being swapped", + "allOf": [ + { + "$ref": "#/definitions/AssetInfo" + } + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "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" + } + } + }, + "query": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "QueryMsg", + "description": "This structure describes the query messages available in the contract.", + "oneOf": [ + { + "description": "Config returns configuration parameters for the contract using a custom [`ConfigResponse`] structure", + "type": "object", + "required": [ + "config" + ], + "properties": { + "config": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "SimulateSwapOperations simulates multi-hop swap operations", + "type": "object", + "required": [ + "simulate_swap_operations" + ], + "properties": { + "simulate_swap_operations": { + "type": "object", + "required": [ + "offer_amount", + "operations" + ], + "properties": { + "offer_amount": { + "description": "The amount of tokens to swap", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] + }, + "operations": { + "description": "The swap operations to perform, each swap involving a specific pool", + "type": "array", + "items": { + "$ref": "#/definitions/SwapOperation" + } + } + }, + "additionalProperties": false + } + }, + "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" + }, + "AssetInfo": { + "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use palomadex::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", + "oneOf": [ + { + "description": "Non-native Token", + "type": "object", + "required": [ + "token" + ], + "properties": { + "token": { + "type": "object", + "required": [ + "contract_addr" + ], + "properties": { + "contract_addr": { + "$ref": "#/definitions/Addr" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Native token", + "type": "object", + "required": [ + "native_token" + ], + "properties": { + "native_token": { + "type": "object", + "required": [ + "denom" + ], + "properties": { + "denom": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "SwapOperation": { + "description": "This enum describes a swap operation.", + "oneOf": [ + { + "description": "Native swap", + "type": "object", + "required": [ + "native_swap" + ], + "properties": { + "native_swap": { + "type": "object", + "required": [ + "ask_denom", + "offer_denom" + ], + "properties": { + "ask_denom": { + "description": "The name (denomination) of the native asset to swap to", + "type": "string" + }, + "offer_denom": { + "description": "The name (denomination) of the native asset to swap from", + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "ASTRO swap", + "type": "object", + "required": [ + "astro_swap" + ], + "properties": { + "astro_swap": { + "type": "object", + "required": [ + "ask_asset_info", + "offer_asset_info" + ], + "properties": { + "ask_asset_info": { + "description": "Information about the asset we swap to", + "allOf": [ + { + "$ref": "#/definitions/AssetInfo" + } + ] + }, + "offer_asset_info": { + "description": "Information about the asset being swapped", + "allOf": [ + { + "$ref": "#/definitions/AssetInfo" + } + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "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" + } + } + }, + "migrate": null, + "sudo": null, + "responses": { + "config": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ConfigResponse", + "description": "This structure describes a custom struct to return a query response containing the base contract configuration.", + "type": "object", + "required": [ + "palomadex_factory" + ], + "properties": { + "palomadex_factory": { + "description": "The Palomadex factory contract address", + "type": "string" + } + }, + "additionalProperties": false + }, + "simulate_swap_operations": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "SimulateSwapOperationsResponse", + "description": "This structure describes a custom struct to return a query response containing the end amount of a swap simulation", + "type": "object", + "required": [ + "amount" + ], + "properties": { + "amount": { + "description": "The amount of tokens received in a swap simulation", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] + } + }, + "additionalProperties": false, + "definitions": { + "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" + } + } + } + } +} diff --git a/schemas/palomadex-router/raw/execute.json b/schemas/palomadex-router/raw/execute.json new file mode 100644 index 0000000..c676f87 --- /dev/null +++ b/schemas/palomadex-router/raw/execute.json @@ -0,0 +1,270 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ExecuteMsg", + "description": "This structure describes the execute messages available in the contract.", + "oneOf": [ + { + "description": "Receive receives a message of type [`Cw20ReceiveMsg`] and processes it depending on the received template", + "type": "object", + "required": [ + "receive" + ], + "properties": { + "receive": { + "$ref": "#/definitions/Cw20ReceiveMsg" + } + }, + "additionalProperties": false + }, + { + "description": "ExecuteSwapOperations processes multiple swaps while mentioning the minimum amount of tokens to receive for the last swap operation", + "type": "object", + "required": [ + "execute_swap_operations" + ], + "properties": { + "execute_swap_operations": { + "type": "object", + "required": [ + "operations" + ], + "properties": { + "max_spread": { + "anyOf": [ + { + "$ref": "#/definitions/Decimal" + }, + { + "type": "null" + } + ] + }, + "minimum_receive": { + "anyOf": [ + { + "$ref": "#/definitions/Uint128" + }, + { + "type": "null" + } + ] + }, + "operations": { + "type": "array", + "items": { + "$ref": "#/definitions/SwapOperation" + } + }, + "to": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Internal use ExecuteSwapOperation executes a single swap operation", + "type": "object", + "required": [ + "execute_swap_operation" + ], + "properties": { + "execute_swap_operation": { + "type": "object", + "required": [ + "operation", + "single" + ], + "properties": { + "max_spread": { + "anyOf": [ + { + "$ref": "#/definitions/Decimal" + }, + { + "type": "null" + } + ] + }, + "operation": { + "$ref": "#/definitions/SwapOperation" + }, + "single": { + "type": "boolean" + }, + "to": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + } + }, + "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" + }, + "AssetInfo": { + "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use palomadex::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", + "oneOf": [ + { + "description": "Non-native Token", + "type": "object", + "required": [ + "token" + ], + "properties": { + "token": { + "type": "object", + "required": [ + "contract_addr" + ], + "properties": { + "contract_addr": { + "$ref": "#/definitions/Addr" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Native token", + "type": "object", + "required": [ + "native_token" + ], + "properties": { + "native_token": { + "type": "object", + "required": [ + "denom" + ], + "properties": { + "denom": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "Binary": { + "description": "Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec. See also .", + "type": "string" + }, + "Cw20ReceiveMsg": { + "description": "Cw20ReceiveMsg should be de/serialized under `Receive()` variant in a ExecuteMsg", + "type": "object", + "required": [ + "amount", + "msg", + "sender" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "msg": { + "$ref": "#/definitions/Binary" + }, + "sender": { + "type": "string" + } + }, + "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" + }, + "SwapOperation": { + "description": "This enum describes a swap operation.", + "oneOf": [ + { + "description": "Native swap", + "type": "object", + "required": [ + "native_swap" + ], + "properties": { + "native_swap": { + "type": "object", + "required": [ + "ask_denom", + "offer_denom" + ], + "properties": { + "ask_denom": { + "description": "The name (denomination) of the native asset to swap to", + "type": "string" + }, + "offer_denom": { + "description": "The name (denomination) of the native asset to swap from", + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "ASTRO swap", + "type": "object", + "required": [ + "astro_swap" + ], + "properties": { + "astro_swap": { + "type": "object", + "required": [ + "ask_asset_info", + "offer_asset_info" + ], + "properties": { + "ask_asset_info": { + "description": "Information about the asset we swap to", + "allOf": [ + { + "$ref": "#/definitions/AssetInfo" + } + ] + }, + "offer_asset_info": { + "description": "Information about the asset being swapped", + "allOf": [ + { + "$ref": "#/definitions/AssetInfo" + } + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "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" + } + } +} diff --git a/schemas/palomadex-router/raw/instantiate.json b/schemas/palomadex-router/raw/instantiate.json new file mode 100644 index 0000000..3691374 --- /dev/null +++ b/schemas/palomadex-router/raw/instantiate.json @@ -0,0 +1,16 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "InstantiateMsg", + "description": "This structure holds the parameters used for creating a contract.", + "type": "object", + "required": [ + "palomadex_factory" + ], + "properties": { + "palomadex_factory": { + "description": "The palomadex factory contract address", + "type": "string" + } + }, + "additionalProperties": false +} diff --git a/schemas/palomadex-router/raw/query.json b/schemas/palomadex-router/raw/query.json new file mode 100644 index 0000000..1944703 --- /dev/null +++ b/schemas/palomadex-router/raw/query.json @@ -0,0 +1,184 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "QueryMsg", + "description": "This structure describes the query messages available in the contract.", + "oneOf": [ + { + "description": "Config returns configuration parameters for the contract using a custom [`ConfigResponse`] structure", + "type": "object", + "required": [ + "config" + ], + "properties": { + "config": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "SimulateSwapOperations simulates multi-hop swap operations", + "type": "object", + "required": [ + "simulate_swap_operations" + ], + "properties": { + "simulate_swap_operations": { + "type": "object", + "required": [ + "offer_amount", + "operations" + ], + "properties": { + "offer_amount": { + "description": "The amount of tokens to swap", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] + }, + "operations": { + "description": "The swap operations to perform, each swap involving a specific pool", + "type": "array", + "items": { + "$ref": "#/definitions/SwapOperation" + } + } + }, + "additionalProperties": false + } + }, + "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" + }, + "AssetInfo": { + "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use palomadex::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", + "oneOf": [ + { + "description": "Non-native Token", + "type": "object", + "required": [ + "token" + ], + "properties": { + "token": { + "type": "object", + "required": [ + "contract_addr" + ], + "properties": { + "contract_addr": { + "$ref": "#/definitions/Addr" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Native token", + "type": "object", + "required": [ + "native_token" + ], + "properties": { + "native_token": { + "type": "object", + "required": [ + "denom" + ], + "properties": { + "denom": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "SwapOperation": { + "description": "This enum describes a swap operation.", + "oneOf": [ + { + "description": "Native swap", + "type": "object", + "required": [ + "native_swap" + ], + "properties": { + "native_swap": { + "type": "object", + "required": [ + "ask_denom", + "offer_denom" + ], + "properties": { + "ask_denom": { + "description": "The name (denomination) of the native asset to swap to", + "type": "string" + }, + "offer_denom": { + "description": "The name (denomination) of the native asset to swap from", + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "ASTRO swap", + "type": "object", + "required": [ + "astro_swap" + ], + "properties": { + "astro_swap": { + "type": "object", + "required": [ + "ask_asset_info", + "offer_asset_info" + ], + "properties": { + "ask_asset_info": { + "description": "Information about the asset we swap to", + "allOf": [ + { + "$ref": "#/definitions/AssetInfo" + } + ] + }, + "offer_asset_info": { + "description": "Information about the asset being swapped", + "allOf": [ + { + "$ref": "#/definitions/AssetInfo" + } + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "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" + } + } +} diff --git a/schemas/palomadex-router/raw/response_to_config.json b/schemas/palomadex-router/raw/response_to_config.json new file mode 100644 index 0000000..6841fef --- /dev/null +++ b/schemas/palomadex-router/raw/response_to_config.json @@ -0,0 +1,16 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ConfigResponse", + "description": "This structure describes a custom struct to return a query response containing the base contract configuration.", + "type": "object", + "required": [ + "palomadex_factory" + ], + "properties": { + "palomadex_factory": { + "description": "The Palomadex factory contract address", + "type": "string" + } + }, + "additionalProperties": false +} diff --git a/schemas/palomadex-router/raw/response_to_simulate_swap_operations.json b/schemas/palomadex-router/raw/response_to_simulate_swap_operations.json new file mode 100644 index 0000000..b874e03 --- /dev/null +++ b/schemas/palomadex-router/raw/response_to_simulate_swap_operations.json @@ -0,0 +1,26 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "SimulateSwapOperationsResponse", + "description": "This structure describes a custom struct to return a query response containing the end amount of a swap simulation", + "type": "object", + "required": [ + "amount" + ], + "properties": { + "amount": { + "description": "The amount of tokens received in a swap simulation", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] + } + }, + "additionalProperties": false, + "definitions": { + "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" + } + } +} diff --git a/scripts/build_release.sh b/scripts/build_release.sh new file mode 100755 index 0000000..c8b893d --- /dev/null +++ b/scripts/build_release.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash + +set -e +set -o pipefail + +projectPath=$(cd "$(dirname "${0}")" && cd ../ && pwd) + +docker run --rm -v "$projectPath":/code \ + --mount type=volume,source="$(basename "$projectPath")_cache",target=/code/target \ + --mount type=volume,source=registry_cache,target=/usr/local/cargo/registry \ + cosmwasm/optimizer:0.16.1 \ No newline at end of file diff --git a/scripts/build_schemas.sh b/scripts/build_schemas.sh new file mode 100755 index 0000000..b719b88 --- /dev/null +++ b/scripts/build_schemas.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash + +set -e + +TARGETS="$(cargo metadata --no-deps --locked --manifest-path "$PWD/Cargo.toml" --format-version 1 | + jq -r --arg contracts "$PWD/contracts" -r \ + '.packages[] + | select(.manifest_path | startswith($contracts)) + | .name + " " + (.targets[] | select(.kind==["example"]) | .name)')" + +rm -rf schemas + +while read -r contract schema_builder; do + if [[ ! "$schema_builder" =~ "_schema" ]]; then + echo "Skipping example $schema_builder" + continue + fi + echo "Building $contract $schema_builder" + cargo run --locked --example "$schema_builder" + + mkdir -p "schemas/$contract" + mv "$PWD/schema/"* "$PWD/schemas/$contract/" +done <<<"$TARGETS" + +rmdir schema diff --git a/scripts/check_artifacts_size.sh b/scripts/check_artifacts_size.sh new file mode 100755 index 0000000..178120f --- /dev/null +++ b/scripts/check_artifacts_size.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash + +set -e +set -o pipefail + +# terra: https://github.com/terra-money/wasmd/blob/2308975f45eac299bdf246737674482eaa51051c/x/wasm/types/validation.go#L12 +# injective: https://github.com/InjectiveLabs/wasmd/blob/e087f275712b5f0a798791495dee0e453d67cad3/x/wasm/types/validation.go#L19 +maximum_size=800 + +for artifact in artifacts/*.wasm; do + artifactsize=$(du -k "$artifact" | cut -f 1) + if [ "$artifactsize" -gt $maximum_size ]; then + echo "Artifact file size exceeded: $artifact" + echo "Artifact size: $artifactsize" + echo "Max size: $maximum_size" + exit 1 + fi +done diff --git a/scripts/coverage.sh b/scripts/coverage.sh new file mode 100755 index 0000000..9b031d0 --- /dev/null +++ b/scripts/coverage.sh @@ -0,0 +1,7 @@ +#!/usr/src/env bash + +# Usage: ./scripts/coverage.sh +# Example: ./scripts/coverage.sh astroport-pair + +cargo tarpaulin --target-dir target/tarpaulin_build --skip-clean --exclude-files *tests*.rs --exclude-files target*.rs \ + -p "$1" --out Html diff --git a/scripts/publish_crates.sh b/scripts/publish_crates.sh new file mode 100755 index 0000000..4a2fe13 --- /dev/null +++ b/scripts/publish_crates.sh @@ -0,0 +1,91 @@ +#!/usr/bin/env bash + +set -eu +set -o pipefail + +declare CONTRACTS +declare ROOT_DIR +declare FIRST_CRATES +declare SKIP_CRATES +declare DRY_FLAGS + +# NOTE: astroport-governance and astro-satellite-package should be published first + +if [ -z "${1:-}" ]; then + echo "Usage: $0 [optional: --publish]" + echo "If flag --publish is not set, only dry-run will be performed." + echo "NOTE: astroport-governance and astro-satellite-package should be published first." + exit 1 +fi + +DRY_FLAGS="--dry-run --allow-dirty" +if [ -z "${2:-}" ]; then + echo "Dry run mode" +else + echo "Publishing mode" + DRY_FLAGS="" +fi + +publish() { + local cargo_error temp_err_file ret_code=0 + local crate="$1" + + echo "Publishing $crate ..." + + set +e + + # Run 'cargo publish' and redirect stderr to a temporary file + temp_err_file="/tmp/cargo-publish-error-$crate.$$" + # shellcheck disable=SC2086 + cargo publish -p "$crate" --locked $DRY_FLAGS 2> >(tee "$temp_err_file") + ret_code=$? + cargo_error="$(<"$temp_err_file")" + rm "$temp_err_file" + + set -e + + # Sleep for 60 seconds if the crate was published successfully + [ $ret_code -eq 0 ] && [ -z "$DRY_FLAGS" ] && sleep 60 + + # Check if the error is related to the crate version already being uploaded + if [[ $cargo_error =~ "the remote server responded with an error" && $cargo_error =~ "is already uploaded" ]]; then + ret_code=0 + fi + + # Skip if the error is related to the crate version not being found in the registry and + # the script is running in dry-run mode + if [[ $cargo_error =~ "no matching package named" || $cargo_error =~ "failed to select a version for the requirement" ]] && \ + [[ -n "$DRY_FLAGS" ]]; then + ret_code=0 + fi + + # Return the original exit code from 'cargo publish' + return $ret_code +} + +ROOT_DIR="$(realpath "$1")" + +FIRST_CRATES="astroport-circular-buffer astroport astroport-factory astroport-pcl-common" +SKIP_CRATES="astroport-pair-astro-xastro astroport-native-coin-wrapper astroport-shared-multisig astroport-token astroport-xastro-outpost-token astroport-pair-xyk-sale-tax astroport-pair-concentrated" + +for contract in $FIRST_CRATES; do + publish "$contract" +done + +CONTRACTS="$(cargo metadata --no-deps --locked --manifest-path "$ROOT_DIR/Cargo.toml" --format-version 1 | + jq -r --arg contracts "$ROOT_DIR/contracts" \ + '.packages[] + | select(.manifest_path | startswith($contracts)) + | .name')" + +echo -e "Publishing crates:\n$CONTRACTS" + +for contract in $CONTRACTS; do + if [[ "$FIRST_CRATES $SKIP_CRATES" == *"$contract"* ]]; then + continue + fi + + publish "$contract" +done + +echo "ALL DONE"