diff --git a/.github/workflows/tests_and_checks.yml b/.github/workflows/tests_and_checks.yml index 1a408045..a0d07f9c 100644 --- a/.github/workflows/tests_and_checks.yml +++ b/.github/workflows/tests_and_checks.yml @@ -1,4 +1,4 @@ -name: Contract tests and checks +name: Tests and Checks on: pull_request: @@ -10,18 +10,21 @@ env: CARGO_TERM_COLOR: always jobs: - test_and_check: - name: Test and check - runs-on: ubuntu-latest - + rust-lint-and-tests: + runs-on: ubuntu-22.04 + strategy: + matrix: + manifest: + - sdk/rust/Cargo.toml steps: - name: Cancel Previous Runs uses: styfle/cancel-workflow-action@0.9.1 with: access_token: ${{ github.token }} - - name: Checkout sources + - name: Check out source uses: actions/checkout@v2 + - uses: actions/cache@v2 with: path: | @@ -33,40 +36,19 @@ jobs: target key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} - - name: Install stable toolchain + - name: Install stable rust toolchain uses: actions-rs/toolchain@v1 with: profile: minimal - toolchain: 1.60.0 - override: true - components: rustfmt, clippy - - - name: Set up Python - uses: actions/setup-python@v2 - with: - python-version: 3.9.7 - - - name: Run tests - uses: actions-rs/cargo@v1 - with: - command: test - args: --no-fail-fast --locked - env: - RUST_BACKTRACE: 1 - - - name: Run cargo clippy - uses: actions-rs/cargo@v1 - with: - command: clippy - args: -- -D warnings - - - name: Run cargo fmt - uses: actions-rs/cargo@v1 - with: - command: fmt - args: --all -- --check - + toolchain: stable + components: clippy, rustfmt + default: true + + - run: cargo fetch --verbose --locked + - run: cargo check --verbose --workspace --locked --all-targets + - run: cargo test --verbose --workspace --locked --no-fail-fast + - run: cargo clippy --all --all-targets -- -D warnings + - run: cargo fmt --all -- --check - run: ./bin/generate_schemas.sh - name: Schema Changes run: git diff --exit-code - diff --git a/.gitignore b/.gitignore index abee01e2..ea8c4bf7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,21 +1 @@ -# macOS -.DS_Store - -# Text file backups -**/*.rs.bk - -# Build results -target/ - -# IDEs -.vscode/ -.idea/ -*.iml - -# Auto-gen -.cargo-ok -/scripts/.env -/scripts/node_modules/ -#/scripts/package-lock.json -/artifacts/ -/contracts/**/schema/ \ No newline at end of file +/target diff --git a/Cargo.lock b/Cargo.lock index e2b9661e..cb099c49 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -275,9 +275,16 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" dependencies = [ + "block-padding", "generic-array", ] +[[package]] +name = "block-padding" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d696c370c750c948ada61c69a0ee2cbbb9c50b1019ddb86d9317157a99c2cae" + [[package]] name = "byteorder" version = "1.4.3" @@ -310,7 +317,7 @@ checksum = "5eb0afef2325df81aadbf9be1233f522ed8f6e91df870c764bc44cca2b1415bd" dependencies = [ "digest", "ed25519-zebra", - "k256", + "k256 0.10.4", "rand_core 0.6.3", "thiserror", ] @@ -376,6 +383,18 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" +[[package]] +name = "crypto-bigint" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83bd3bb4314701c568e340cd8cf78c975aa0ca79e03d3f6d1677d5b0c9c0c03" +dependencies = [ + "generic-array", + "rand_core 0.6.3", + "subtle", + "zeroize", +] + [[package]] name = "crypto-bigint" version = "0.3.2" @@ -442,9 +461,9 @@ dependencies = [ [[package]] name = "cw-storage-plus" -version = "0.13.2" +version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9336ecef1e19d56cf6e3e932475fc6a3dee35eec5a386e07917a1d1ba6bb0e35" +checksum = "648b1507290bbc03a8d88463d7cd9b04b1fa0155e5eef366c4fa052b9caaac7a" dependencies = [ "cosmwasm-std", "schemars", @@ -453,9 +472,21 @@ dependencies = [ [[package]] name = "cw-utils" -version = "0.13.2" +version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "babd2c090f39d07ce5bf2556962305e795daa048ce20a93709eb591476e4a29e" +checksum = "9dbaecb78c8e8abfd6b4258c7f4fbeb5c49a5e45ee4d910d3240ee8e1d714e1b" +dependencies = [ + "cosmwasm-std", + "schemars", + "serde", + "thiserror", +] + +[[package]] +name = "cw0" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae676b6cced78a3d38ad4b01ab4ed66fc78ac191c3c0d6bfd5372cb2efd473b" dependencies = [ "cosmwasm-std", "schemars", @@ -476,9 +507,9 @@ dependencies = [ [[package]] name = "cw2" -version = "0.13.2" +version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "993df11574f29574dd443eb0c189484bb91bc0638b6de3e32ab7f9319c92122d" +checksum = "04cf4639517490dd36b333bbd6c4fbd92e325fd0acf4683b41753bc5eb63bfc1" dependencies = [ "cosmwasm-std", "cw-storage-plus", @@ -488,9 +519,9 @@ dependencies = [ [[package]] name = "cw20" -version = "0.13.2" +version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "356d364602c5fe763544ea00d485b825d6ef519a2fc6a3145528d7df3a603f40" +checksum = "4cb782b8f110819a4eb5dbbcfed25ffba49ec16bbe32b4ad8da50a5ce68fec05" dependencies = [ "cosmwasm-std", "cw-utils", @@ -500,9 +531,9 @@ dependencies = [ [[package]] name = "cw20-base" -version = "0.13.2" +version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b370e5fb6b35db58cabd7903b8f9bd4c3bcff701c63bf1a7185a84e60bfa502" +checksum = "0306e606581f4fb45e82bcbb7f0333179ed53dd949c6523f01a99b4bfc1475a0" dependencies = [ "cosmwasm-std", "cw-storage-plus", @@ -514,6 +545,84 @@ dependencies = [ "thiserror", ] +[[package]] +name = "cw20-legacy" +version = "0.2.0" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cosmwasm-storage", + "cw-storage-plus", + "cw-utils", + "cw2", + "cw20", + "cw20-base", + "schemars", + "serde", + "thiserror", +] + +[[package]] +name = "cw20-wrapped" +version = "0.1.0" +dependencies = [ + "cosmwasm-std", + "cosmwasm-storage", + "cw-storage-plus", + "cw2", + "cw20", + "cw20-legacy", + "schemars", + "serde", + "thiserror", +] + +[[package]] +name = "cw721" +version = "0.10.1" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw0", + "schemars", + "serde", +] + +[[package]] +name = "cw721-base" +version = "0.10.1" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-storage-plus", + "cw0", + "cw2", + "cw721", + "schemars", + "serde", + "thiserror", +] + +[[package]] +name = "cw721-wrapped" +version = "0.10.1" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cosmwasm-storage", + "cw2", + "cw721", + "cw721-base", + "schemars", + "serde", +] + +[[package]] +name = "der" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79b71cca7d95d7681a4b3b9cdf63c8dbc3730d0584c2c74e31416d64a90493f4" + [[package]] name = "der" version = "0.5.1" @@ -549,14 +658,26 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "21e50f3adc76d6a43f5ed73b698a87d0760ca74617f60f7c3b879003536fdd28" +[[package]] +name = "ecdsa" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43ee23aa5b4f68c7a092b5c3beb25f50c406adc75e2363634f242f28ab255372" +dependencies = [ + "der 0.4.5", + "elliptic-curve 0.10.6", + "hmac", + "signature", +] + [[package]] name = "ecdsa" version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0d69ae62e0ce582d56380743515fefaf1a8c70cec685d9677636d7e30ae9dc9" dependencies = [ - "der", - "elliptic-curve", + "der 0.5.1", + "elliptic-curve 0.11.12", "rfc6979", "signature", ] @@ -582,6 +703,21 @@ version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" +[[package]] +name = "elliptic-curve" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "beca177dcb8eb540133e7680baff45e7cc4d93bf22002676cec549f82343721b" +dependencies = [ + "crypto-bigint 0.2.11", + "ff 0.10.1", + "generic-array", + "group 0.10.0", + "rand_core 0.6.3", + "subtle", + "zeroize", +] + [[package]] name = "elliptic-curve" version = "0.11.12" @@ -589,11 +725,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25b477563c2bfed38a3b7a60964c49e058b2510ad3f12ba3483fd8f62c2306d6" dependencies = [ "base16ct", - "crypto-bigint", - "der", - "ff", + "crypto-bigint 0.3.2", + "der 0.5.1", + "ff 0.11.1", "generic-array", - "group", + "group 0.11.0", "rand_core 0.6.3", "sec1", "subtle", @@ -609,6 +745,16 @@ dependencies = [ "instant", ] +[[package]] +name = "ff" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0f40b2dcd8bc322217a5f6559ae5f9e9d1de202a2ecee2e9eafcbece7562a4f" +dependencies = [ + "rand_core 0.6.3", + "subtle", +] + [[package]] name = "ff" version = "0.11.1" @@ -674,13 +820,24 @@ dependencies = [ "syn", ] +[[package]] +name = "group" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c363a5301b8f153d80747126a04b3c82073b9fe3130571a9d170cacdeaf7912" +dependencies = [ + "ff 0.10.1", + "rand_core 0.6.3", + "subtle", +] + [[package]] name = "group" version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc5ac374b108929de78460075f3dc439fa66df9d8fc77e8f12caa5165fcf0c89" dependencies = [ - "ff", + "ff 0.11.1", "rand_core 0.6.3", "subtle", ] @@ -770,6 +927,17 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "112c678d4050afce233f4f2852bb2eb519230b3cf12f33585275537d7e41578d" +[[package]] +name = "k256" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "903ae2481bcdfdb7b68e0a9baa4b7c9aff600b9ae2e8e5bb5833b8c91ab851ea" +dependencies = [ + "cfg-if", + "ecdsa 0.12.4", + "elliptic-curve 0.10.6", +] + [[package]] name = "k256" version = "0.10.4" @@ -777,12 +945,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19c3a5e0a0b8450278feda242592512e09f61c72e018b8cd5c859482802daf2d" dependencies = [ "cfg-if", - "ecdsa", - "elliptic-curve", + "ecdsa 0.13.4", + "elliptic-curve 0.11.12", "sec1", "sha2", ] +[[package]] +name = "keccak" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9b7d56ba4a8344d6be9729995e6b06f928af29998cdf79fe390cbf6b1fee838" + [[package]] name = "lazy_static" version = "1.4.0" @@ -805,6 +979,24 @@ dependencies = [ "scopeguard", ] +[[package]] +name = "nft-bridge" +version = "0.1.0" +dependencies = [ + "cosmwasm-std", + "cosmwasm-storage", + "cw721", + "cw721-base", + "cw721-wrapped", + "hex", + "lazy_static", + "schemars", + "serde", + "serde_json", + "sha3", + "wormhole-bridge-terra", +] + [[package]] name = "num-traits" version = "0.2.15" @@ -880,7 +1072,7 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7cabda3fb821068a9a4fab19a683eac3af12edf0f34b94a8be53c4972b8149d0" dependencies = [ - "der", + "der 0.5.1", "spki", "zeroize", ] @@ -1156,7 +1348,7 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96ef608575f6392792f9ecf7890c00086591d29a83910939d430753f7c050525" dependencies = [ - "crypto-bigint", + "crypto-bigint 0.3.2", "hmac", "zeroize", ] @@ -1215,7 +1407,7 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08da66b8b0965a5555b6bd6639e68ccba85e1e2506f5fbb089e93f8a04e1a2d1" dependencies = [ - "der", + "der 0.5.1", "generic-array", "pkcs8", "subtle", @@ -1286,11 +1478,23 @@ dependencies = [ "opaque-debug", ] +[[package]] +name = "sha3" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f81199417d4e5de3f04b1e871023acea7389672c4135918f05aa9cbf2f2fa809" +dependencies = [ + "block-buffer", + "digest", + "keccak", + "opaque-debug", +] + [[package]] name = "signature" -version = "1.4.0" +version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02658e48d89f2bec991f9a78e69cfa4c316f8d6a6c4ec12fae1aeb263d486788" +checksum = "f2807892cfa58e081aa1f1111391c7a0649d4fa127a4ffbe34bcbfb35a1171a4" dependencies = [ "digest", "rand_core 0.6.3", @@ -1317,7 +1521,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44d01ac02a6ccf3e07db148d2be087da624fea0221a16152ed01f0496a6b0a27" dependencies = [ "base64ct", - "der", + "der 0.5.1", ] [[package]] @@ -1456,8 +1660,28 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "wormhole-bridge-terra" +version = "0.1.0" +dependencies = [ + "cosmwasm-std", + "cosmwasm-storage", + "cw20", + "cw20-base", + "generic-array", + "getrandom 0.2.6", + "hex", + "k256 0.9.6", + "lazy_static", + "schemars", + "serde", + "serde_json", + "sha3", + "thiserror", +] + [[package]] name = "zeroize" -version = "1.5.5" +version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94693807d016b2f2d2e14420eb3bfcca689311ff775dcf113d74ea624b7cdf07" +checksum = "d68d9dcec5f9b43a30d38c49f91dfedfaac384cb8f085faca366c26207dd1619" diff --git a/Cargo.toml b/Cargo.toml index 01a60eee..4e3cb969 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,8 @@ [workspace] resolver = "2" members = [ +# "contracts/mock-bridge-integration", +# "contracts/token-bridge", "contracts/factory", "contracts/oracle", "contracts/pair", @@ -14,6 +16,13 @@ members = [ "contracts/tokenomics/xastro_token", "contracts/whitelist", "packages/*", + "packages/cw721", + "wormhole/cw20-legacy", + "wormhole/cw20-wrapped", + "wormhole/cw721-base", + "wormhole/cw721-wrapped", + "wormhole/nft-bridge", + "wormhole/wormhole", ] [profile.release] diff --git a/contracts/factory/schema/config_response.json b/contracts/factory/schema/config_response.json new file mode 100644 index 00000000..d0d99fe5 --- /dev/null +++ b/contracts/factory/schema/config_response.json @@ -0,0 +1,161 @@ +{ + "$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": [ + "owner", + "pair_configs", + "token_code_id", + "whitelist_code_id" + ], + "properties": { + "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 Astroport 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 Astroport LP tokens", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "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", + "is_disabled", + "is_generator_disabled", + "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", + "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", + "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" + } + ] + }, + "total_fee_bps": { + "description": "The total fees (in bps) charged by a pair of this type", + "type": "integer", + "format": "uint16", + "minimum": 0.0 + } + } + }, + "PairType": { + "description": "This enum describes available pair types. ## Available pool types ``` # use astroport::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 + }, + { + "description": "Stable pair type", + "type": "object", + "required": [ + "stable" + ], + "properties": { + "stable": { + "type": "object" + } + }, + "additionalProperties": false + }, + { + "description": "Custom pair type", + "type": "object", + "required": [ + "custom" + ], + "properties": { + "custom": { + "type": "string" + } + }, + "additionalProperties": false + } + ] + } + } +} diff --git a/contracts/factory/schema/execute_msg.json b/contracts/factory/schema/execute_msg.json new file mode 100644 index 00000000..e35da646 --- /dev/null +++ b/contracts/factory/schema/execute_msg.json @@ -0,0 +1,382 @@ +{ + "$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": { + "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 Astroport 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 Astroport LP tokens", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + } + } + } + }, + "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 + }, + { + "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 two assets to create the pool for", + "type": "array", + "items": { + "$ref": "#/definitions/AssetInfo" + }, + "maxItems": 2, + "minItems": 2 + }, + "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 + }, + { + "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" + }, + "maxItems": 2, + "minItems": 2 + } + } + } + }, + "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 + }, + { + "description": "DropOwnershipProposal removes the existing offer to change contract ownership.", + "type": "object", + "required": [ + "drop_ownership_proposal" + ], + "properties": { + "drop_ownership_proposal": { + "type": "object" + } + }, + "additionalProperties": false + }, + { + "description": "Used to claim contract ownership.", + "type": "object", + "required": [ + "claim_ownership" + ], + "properties": { + "claim_ownership": { + "type": "object" + } + }, + "additionalProperties": false + }, + { + "description": "MarkAsMigrated marks pairs as migrated", + "type": "object", + "required": [ + "mark_as_migrated" + ], + "properties": { + "mark_as_migrated": { + "type": "object", + "required": [ + "pairs" + ], + "properties": { + "pairs": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + }, + "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 astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"paloma...\") }; 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 + }, + { + "description": "Native token", + "type": "object", + "required": [ + "native_token" + ], + "properties": { + "native_token": { + "type": "object", + "required": [ + "denom" + ], + "properties": { + "denom": { + "type": "string" + } + } + } + }, + "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", + "type": "string" + }, + "PairConfig": { + "description": "This structure stores a pair type's configuration.", + "type": "object", + "required": [ + "code_id", + "is_disabled", + "is_generator_disabled", + "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", + "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", + "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" + } + ] + }, + "total_fee_bps": { + "description": "The total fees (in bps) charged by a pair of this type", + "type": "integer", + "format": "uint16", + "minimum": 0.0 + } + } + }, + "PairType": { + "description": "This enum describes available pair types. ## Available pool types ``` # use astroport::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 + }, + { + "description": "Stable pair type", + "type": "object", + "required": [ + "stable" + ], + "properties": { + "stable": { + "type": "object" + } + }, + "additionalProperties": false + }, + { + "description": "Custom pair type", + "type": "object", + "required": [ + "custom" + ], + "properties": { + "custom": { + "type": "string" + } + }, + "additionalProperties": false + } + ] + } + } +} diff --git a/contracts/factory/schema/instantiate_msg.json b/contracts/factory/schema/instantiate_msg.json new file mode 100644 index 00000000..22ab8450 --- /dev/null +++ b/contracts/factory/schema/instantiate_msg.json @@ -0,0 +1,145 @@ +{ + "$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": [ + "owner", + "pair_configs", + "token_code_id", + "whitelist_code_id" + ], + "properties": { + "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 Astroport LP tokens", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "definitions": { + "PairConfig": { + "description": "This structure stores a pair type's configuration.", + "type": "object", + "required": [ + "code_id", + "is_disabled", + "is_generator_disabled", + "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", + "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", + "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" + } + ] + }, + "total_fee_bps": { + "description": "The total fees (in bps) charged by a pair of this type", + "type": "integer", + "format": "uint16", + "minimum": 0.0 + } + } + }, + "PairType": { + "description": "This enum describes available pair types. ## Available pool types ``` # use astroport::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 + }, + { + "description": "Stable pair type", + "type": "object", + "required": [ + "stable" + ], + "properties": { + "stable": { + "type": "object" + } + }, + "additionalProperties": false + }, + { + "description": "Custom pair type", + "type": "object", + "required": [ + "custom" + ], + "properties": { + "custom": { + "type": "string" + } + }, + "additionalProperties": false + } + ] + } + } +} diff --git a/contracts/factory/schema/migrate_msg.json b/contracts/factory/schema/migrate_msg.json new file mode 100644 index 00000000..af86f16c --- /dev/null +++ b/contracts/factory/schema/migrate_msg.json @@ -0,0 +1,20 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "MigrateMsg", + "description": "This structure stores the parameters used in a migration message.", + "type": "object", + "required": [ + "params" + ], + "properties": { + "params": { + "$ref": "#/definitions/Binary" + } + }, + "definitions": { + "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", + "type": "string" + } + } +} diff --git a/contracts/factory/schema/pair_info.json b/contracts/factory/schema/pair_info.json new file mode 100644 index 00000000..ce5d9c2b --- /dev/null +++ b/contracts/factory/schema/pair_info.json @@ -0,0 +1,144 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "PairInfo", + "description": "This structure stores the main parameters for an Astroport pair", + "type": "object", + "required": [ + "asset_infos", + "contract_addr", + "liquidity_token", + "pair_type" + ], + "properties": { + "asset_infos": { + "description": "Asset information for the two assets in the pool", + "type": "array", + "items": { + "$ref": "#/definitions/AssetInfo" + }, + "maxItems": 2, + "minItems": 2 + }, + "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" + } + ] + } + }, + "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 astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"paloma...\") }; 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 + }, + { + "description": "Native token", + "type": "object", + "required": [ + "native_token" + ], + "properties": { + "native_token": { + "type": "object", + "required": [ + "denom" + ], + "properties": { + "denom": { + "type": "string" + } + } + } + }, + "additionalProperties": false + } + ] + }, + "PairType": { + "description": "This enum describes available pair types. ## Available pool types ``` # use astroport::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 + }, + { + "description": "Stable pair type", + "type": "object", + "required": [ + "stable" + ], + "properties": { + "stable": { + "type": "object" + } + }, + "additionalProperties": false + }, + { + "description": "Custom pair type", + "type": "object", + "required": [ + "custom" + ], + "properties": { + "custom": { + "type": "string" + } + }, + "additionalProperties": false + } + ] + } + } +} diff --git a/contracts/factory/schema/pairs_response.json b/contracts/factory/schema/pairs_response.json new file mode 100644 index 00000000..3195c55d --- /dev/null +++ b/contracts/factory/schema/pairs_response.json @@ -0,0 +1,160 @@ +{ + "$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" + } + } + }, + "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 astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"paloma...\") }; 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 + }, + { + "description": "Native token", + "type": "object", + "required": [ + "native_token" + ], + "properties": { + "native_token": { + "type": "object", + "required": [ + "denom" + ], + "properties": { + "denom": { + "type": "string" + } + } + } + }, + "additionalProperties": false + } + ] + }, + "PairInfo": { + "description": "This structure stores the main parameters for an Astroport pair", + "type": "object", + "required": [ + "asset_infos", + "contract_addr", + "liquidity_token", + "pair_type" + ], + "properties": { + "asset_infos": { + "description": "Asset information for the two assets in the pool", + "type": "array", + "items": { + "$ref": "#/definitions/AssetInfo" + }, + "maxItems": 2, + "minItems": 2 + }, + "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" + } + ] + } + } + }, + "PairType": { + "description": "This enum describes available pair types. ## Available pool types ``` # use astroport::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 + }, + { + "description": "Stable pair type", + "type": "object", + "required": [ + "stable" + ], + "properties": { + "stable": { + "type": "object" + } + }, + "additionalProperties": false + }, + { + "description": "Custom pair type", + "type": "object", + "required": [ + "custom" + ], + "properties": { + "custom": { + "type": "string" + } + }, + "additionalProperties": false + } + ] + } + } +} diff --git a/contracts/factory/schema/query_msg.json b/contracts/factory/schema/query_msg.json new file mode 100644 index 00000000..2d2ebf8d --- /dev/null +++ b/contracts/factory/schema/query_msg.json @@ -0,0 +1,232 @@ +{ + "$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 + }, + { + "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" + }, + "maxItems": 2, + "minItems": 2 + } + } + } + }, + "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 two [`AssetInfo`] elements.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/AssetInfo" + }, + "maxItems": 2, + "minItems": 2 + } + } + } + }, + "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 + }, + { + "description": "Returns a vector that contains blacklisted pair types", + "type": "object", + "required": [ + "blacklisted_pair_types" + ], + "properties": { + "blacklisted_pair_types": { + "type": "object" + } + }, + "additionalProperties": false + }, + { + "description": "Returns a vector that contains pair addresses that are not migrated", + "type": "object", + "required": [ + "pairs_to_migrate" + ], + "properties": { + "pairs_to_migrate": { + "type": "object" + } + }, + "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 astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"paloma...\") }; 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 + }, + { + "description": "Native token", + "type": "object", + "required": [ + "native_token" + ], + "properties": { + "native_token": { + "type": "object", + "required": [ + "denom" + ], + "properties": { + "denom": { + "type": "string" + } + } + } + }, + "additionalProperties": false + } + ] + }, + "PairType": { + "description": "This enum describes available pair types. ## Available pool types ``` # use astroport::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 + }, + { + "description": "Stable pair type", + "type": "object", + "required": [ + "stable" + ], + "properties": { + "stable": { + "type": "object" + } + }, + "additionalProperties": false + }, + { + "description": "Custom pair type", + "type": "object", + "required": [ + "custom" + ], + "properties": { + "custom": { + "type": "string" + } + }, + "additionalProperties": false + } + ] + } + } +} diff --git a/contracts/factory/src/mock_querier.rs b/contracts/factory/src/mock_querier.rs index 4b2de85a..628894da 100644 --- a/contracts/factory/src/mock_querier.rs +++ b/contracts/factory/src/mock_querier.rs @@ -71,7 +71,7 @@ impl WasmMockQuerier { pub fn handle_query(&self, request: &QueryRequest) -> QuerierResult { match &request { QueryRequest::Wasm(WasmQuery::Smart {contract_addr, msg})// => { - => match from_binary(&msg).unwrap() { + => match from_binary(msg).unwrap() { QueryMsg::Pair {} => { let pair_info: PairInfo = match self.astroport_pair_querier.pairs.get(contract_addr) { diff --git a/contracts/factory/src/testing.rs b/contracts/factory/src/testing.rs index 0b90b966..ea12cd5b 100644 --- a/contracts/factory/src/testing.rs +++ b/contracts/factory/src/testing.rs @@ -54,10 +54,9 @@ fn proper_initialization() { whitelist_code_id: 234u64, }; - let env = mock_env(); let info = mock_info("addr0000", &[]); - let res = instantiate(deps.as_mut(), env.clone(), info, msg).unwrap_err(); + let res = instantiate(deps.as_mut(), mock_env(), info, msg).unwrap_err(); assert_eq!(res, ContractError::PairConfigDuplicate {}); let msg = InstantiateMsg { @@ -76,10 +75,9 @@ fn proper_initialization() { whitelist_code_id: 234u64, }; - let env = mock_env(); let info = mock_info("addr0000", &[]); - let res = instantiate(deps.as_mut(), env.clone(), info, msg).unwrap_err(); + let res = instantiate(deps.as_mut(), mock_env(), info, msg).unwrap_err(); assert_eq!(res, ContractError::PairConfigInvalidFeeBps {}); let mut deps = mock_dependencies(&[]); @@ -110,12 +108,11 @@ fn proper_initialization() { whitelist_code_id: 234u64, }; - let env = mock_env(); let info = mock_info("addr0000", &[]); - instantiate(deps.as_mut(), env.clone(), info, msg.clone()).unwrap(); + instantiate(deps.as_mut(), mock_env(), info, msg.clone()).unwrap(); - let query_res = query(deps.as_ref(), env, QueryMsg::Config {}).unwrap(); + let query_res = query(deps.as_ref(), mock_env(), QueryMsg::Config {}).unwrap(); let config_res: ConfigResponse = from_binary(&query_res).unwrap(); assert_eq!(123u64, config_res.token_code_id); assert_eq!(msg.pair_configs, config_res.pair_configs); @@ -137,7 +134,7 @@ fn update_config() { }]; let msg = InstantiateMsg { - pair_configs: pair_configs.clone(), + pair_configs, token_code_id: 123u64, fee_address: None, owner: owner.to_string(), @@ -145,14 +142,12 @@ fn update_config() { whitelist_code_id: 234u64, }; - 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(); + let _res = instantiate(deps.as_mut(), mock_env(), info, msg).unwrap(); // Update config - let env = mock_env(); let info = mock_info(owner, &[]); let msg = ExecuteMsg::UpdateConfig { token_code_id: Some(200u64), @@ -161,11 +156,11 @@ fn update_config() { whitelist_code_id: None, }; - let res = execute(deps.as_mut(), env.clone(), info, msg).unwrap(); + let res = execute(deps.as_mut(), mock_env(), 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 query_res = query(deps.as_ref(), mock_env(), QueryMsg::Config {}).unwrap(); let config_res: ConfigResponse = from_binary(&query_res).unwrap(); assert_eq!(200u64, config_res.token_code_id); assert_eq!(owner, config_res.owner); @@ -179,7 +174,6 @@ fn update_config() { ); // Unauthorized err - let env = mock_env(); let info = mock_info("addr0000", &[]); let msg = ExecuteMsg::UpdateConfig { token_code_id: None, @@ -188,7 +182,7 @@ fn update_config() { whitelist_code_id: None, }; - let res = execute(deps.as_mut(), env.clone(), info, msg).unwrap_err(); + let res = execute(deps.as_mut(), mock_env(), info, msg).unwrap_err(); assert_eq!(res, ContractError::Unauthorized {}); } @@ -206,16 +200,14 @@ fn update_owner() { whitelist_code_id: 234u64, }; - 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(); + instantiate(deps.as_mut(), mock_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 @@ -224,14 +216,14 @@ fn update_owner() { let info = mock_info(new_owner.as_str(), &[]); // Unauthorized check - let err = execute(deps.as_mut(), env.clone(), info, msg.clone()).unwrap_err(); + let err = execute(deps.as_mut(), mock_env(), 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(), + mock_env(), info, ExecuteMsg::ClaimOwnership {}, ) @@ -239,14 +231,14 @@ fn update_owner() { // Propose new owner let info = mock_info(owner, &[]); - let res = execute(deps.as_mut(), env.clone(), info, msg).unwrap(); + let res = execute(deps.as_mut(), mock_env(), 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(), + mock_env(), info, ExecuteMsg::ClaimOwnership {}, ) @@ -257,7 +249,7 @@ fn update_owner() { let info = mock_info(new_owner.as_str(), &[]); let res = execute( deps.as_mut(), - env.clone(), + mock_env(), info, ExecuteMsg::ClaimOwnership {}, ) @@ -266,7 +258,7 @@ fn update_owner() { // Let's query the state let config: ConfigResponse = - from_binary(&query(deps.as_ref(), env.clone(), QueryMsg::Config {}).unwrap()).unwrap(); + from_binary(&query(deps.as_ref(), mock_env(), QueryMsg::Config {}).unwrap()).unwrap(); assert_eq!(new_owner, config.owner); } @@ -292,14 +284,13 @@ fn update_pair_config() { whitelist_code_id: 234u64, }; - 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(); + instantiate(deps.as_mut(), mock_env(), info, msg).unwrap(); // It worked, let's query the state - let query_res = query(deps.as_ref(), env.clone(), QueryMsg::Config {}).unwrap(); + let query_res = query(deps.as_ref(), mock_env(), QueryMsg::Config {}).unwrap(); let config_res: ConfigResponse = from_binary(&query_res).unwrap(); assert_eq!(pair_configs, config_res.pair_configs); @@ -314,18 +305,16 @@ fn update_pair_config() { }; // 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(); + let res = execute(deps.as_mut(), mock_env(), 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.clone(), &[]); + let info = mock_info(owner, &[]); let msg = ExecuteMsg::UpdatePairConfig { config: PairConfig { code_id: 123u64, @@ -337,19 +326,19 @@ fn update_pair_config() { }, }; - let res = execute(deps.as_mut(), env.clone(), info, msg).unwrap_err(); + let res = execute(deps.as_mut(), mock_env(), info, msg).unwrap_err(); assert_eq!(res, ContractError::PairConfigInvalidFeeBps {}); - let info = mock_info(owner.clone(), &[]); + let info = mock_info(owner, &[]); let msg = ExecuteMsg::UpdatePairConfig { config: pair_config.clone(), }; - let res = execute(deps.as_mut(), env.clone(), info, msg).unwrap(); + let res = execute(deps.as_mut(), mock_env(), 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 query_res = query(deps.as_ref(), mock_env(), QueryMsg::Config {}).unwrap(); let config_res: ConfigResponse = from_binary(&query_res).unwrap(); assert_eq!(vec![pair_config.clone()], config_res.pair_configs); @@ -363,18 +352,18 @@ fn update_pair_config() { is_generator_disabled: false, }; - let info = mock_info(owner.clone(), &[]); + let info = mock_info(owner, &[]); let msg = ExecuteMsg::UpdatePairConfig { config: pair_config_custom.clone(), }; - execute(deps.as_mut(), env.clone(), info, msg).unwrap(); + execute(deps.as_mut(), mock_env(), info, msg).unwrap(); // It worked, let's query the state - let query_res = query(deps.as_ref(), env.clone(), QueryMsg::Config {}).unwrap(); + let query_res = query(deps.as_ref(), mock_env(), QueryMsg::Config {}).unwrap(); let config_res: ConfigResponse = from_binary(&query_res).unwrap(); assert_eq!( - vec![pair_config_custom.clone(), pair_config.clone()], + vec![pair_config_custom, pair_config], config_res.pair_configs ); } @@ -401,11 +390,10 @@ fn create_pair() { whitelist_code_id: 234u64, }; - 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 _res = instantiate(deps.as_mut(), mock_env(), info, msg.clone()).unwrap(); let asset_infos = [ AssetInfo::Token { @@ -417,13 +405,12 @@ fn create_pair() { ]; 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(), + mock_env(), info.clone(), ExecuteMsg::CreatePair { pair_type: PairType::Stable {}, @@ -436,7 +423,7 @@ fn create_pair() { let res = execute( deps.as_mut(), - env, + mock_env(), info, ExecuteMsg::CreatePair { pair_type: PairType::Xyk {}, @@ -459,7 +446,7 @@ fn create_pair() { msg: WasmMsg::Instantiate { msg: to_binary(&PairInstantiateMsg { factory_addr: String::from(MOCK_CONTRACT_ADDR), - asset_infos: asset_infos.clone(), + asset_infos, token_code_id: msg.token_code_id, init_params: None }) @@ -498,9 +485,8 @@ fn register() { whitelist_code_id: 234u64, }; - let env = mock_env(); let info = mock_info("addr0000", &[]); - let _res = instantiate(deps.as_mut(), env, info, msg).unwrap(); + let _res = instantiate(deps.as_mut(), mock_env(), info, msg).unwrap(); let asset_infos = [ AssetInfo::Token { @@ -517,9 +503,8 @@ fn register() { init_params: None, }; - let env = mock_env(); let info = mock_info("addr0000", &[]); - let _res = execute(deps.as_mut(), env.clone(), info, msg).unwrap(); + let _res = execute(deps.as_mut(), mock_env(), info, msg).unwrap(); let pair0_addr = "pair0000".to_string(); let pair0_info = PairInfo { @@ -555,7 +540,7 @@ fn register() { let query_res = query( deps.as_ref(), - env.clone(), + mock_env(), QueryMsg::Pair { asset_infos: asset_infos.clone(), }, @@ -593,9 +578,8 @@ fn register() { init_params: None, }; - let env = mock_env(); let info = mock_info("addr0000", &[]); - let _res = execute(deps.as_mut(), env.clone(), info, msg).unwrap(); + let _res = execute(deps.as_mut(), mock_env(), info, msg).unwrap(); let pair1_addr = "pair0001".to_string(); let pair1_info = PairInfo { @@ -627,14 +611,14 @@ fn register() { }), }; - let _res = reply(deps.as_mut(), mock_env(), reply_msg_2.clone()).unwrap(); + let _res = reply(deps.as_mut(), mock_env(), reply_msg_2).unwrap(); let query_msg = QueryMsg::Pairs { start_after: None, limit: None, }; - let res = query(deps.as_ref(), env.clone(), query_msg).unwrap(); + let res = query(deps.as_ref(), mock_env(), query_msg).unwrap(); let pairs_res: PairsResponse = from_binary(&res).unwrap(); assert_eq!( pairs_res.pairs, @@ -659,7 +643,7 @@ fn register() { limit: Some(1), }; - let res = query(deps.as_ref(), env.clone(), query_msg).unwrap(); + let res = query(deps.as_ref(), mock_env(), query_msg).unwrap(); let pairs_res: PairsResponse = from_binary(&res).unwrap(); assert_eq!( pairs_res.pairs, @@ -676,7 +660,7 @@ fn register() { limit: None, }; - let res = query(deps.as_ref(), env.clone(), query_msg).unwrap(); + let res = query(deps.as_ref(), mock_env(), query_msg).unwrap(); let pairs_res: PairsResponse = from_binary(&res).unwrap(); assert_eq!( pairs_res.pairs, @@ -689,11 +673,10 @@ fn register() { ); // Deregister from wrong acc - let env = mock_env(); let info = mock_info("wrong_addr0000", &[]); let res = execute( deps.as_mut(), - env.clone(), + mock_env(), info, ExecuteMsg::Deregister { asset_infos: asset_infos_2.clone(), @@ -704,14 +687,13 @@ fn register() { assert_eq!(res, ContractError::Unauthorized {}); // Proper deregister - let env = mock_env(); - let info = mock_info(owner.clone(), &[]); + let info = mock_info(owner, &[]); let res = execute( deps.as_mut(), - env.clone(), + mock_env(), info, ExecuteMsg::Deregister { - asset_infos: asset_infos_2.clone(), + asset_infos: asset_infos_2, }, ) .unwrap(); @@ -723,14 +705,14 @@ fn register() { limit: None, }; - let res = query(deps.as_ref(), env.clone(), query_msg).unwrap(); + let res = query(deps.as_ref(), mock_env(), query_msg).unwrap(); let pairs_res: PairsResponse = from_binary(&res).unwrap(); assert_eq!( pairs_res.pairs, vec![PairInfo { liquidity_token: Addr::unchecked("liquidity0000"), contract_addr: Addr::unchecked("pair0000"), - asset_infos: asset_infos.clone(), + asset_infos, pair_type: PairType::Xyk {}, },] ); diff --git a/contracts/oracle/schema/execute_msg.json b/contracts/oracle/schema/execute_msg.json new file mode 100644 index 00000000..2e9ed88d --- /dev/null +++ b/contracts/oracle/schema/execute_msg.json @@ -0,0 +1,20 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ExecuteMsg", + "description": "This structure describes the execute functions available in the contract.", + "oneOf": [ + { + "description": "Update/accumulate prices", + "type": "object", + "required": [ + "update" + ], + "properties": { + "update": { + "type": "object" + } + }, + "additionalProperties": false + } + ] +} diff --git a/contracts/oracle/schema/instantiate_msg.json b/contracts/oracle/schema/instantiate_msg.json new file mode 100644 index 00000000..5ce6f382 --- /dev/null +++ b/contracts/oracle/schema/instantiate_msg.json @@ -0,0 +1,78 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "InstantiateMsg", + "description": "This structure stores general parameters for the contract.", + "type": "object", + "required": [ + "asset_infos", + "factory_contract" + ], + "properties": { + "asset_infos": { + "description": "The assets that have a pool for which this contract provides price feeds", + "type": "array", + "items": { + "$ref": "#/definitions/AssetInfo" + }, + "maxItems": 2, + "minItems": 2 + }, + "factory_contract": { + "description": "The factory contract address", + "type": "string" + } + }, + "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 astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"paloma...\") }; 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 + }, + { + "description": "Native token", + "type": "object", + "required": [ + "native_token" + ], + "properties": { + "native_token": { + "type": "object", + "required": [ + "denom" + ], + "properties": { + "denom": { + "type": "string" + } + } + } + }, + "additionalProperties": false + } + ] + } + } +} diff --git a/contracts/oracle/schema/migrate_msg.json b/contracts/oracle/schema/migrate_msg.json new file mode 100644 index 00000000..18c68d87 --- /dev/null +++ b/contracts/oracle/schema/migrate_msg.json @@ -0,0 +1,6 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "MigrateMsg", + "description": "This structure describes a migration message. We currently take no arguments for migrations.", + "type": "object" +} diff --git a/contracts/oracle/schema/query_msg.json b/contracts/oracle/schema/query_msg.json new file mode 100644 index 00000000..5cd040f5 --- /dev/null +++ b/contracts/oracle/schema/query_msg.json @@ -0,0 +1,99 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "QueryMsg", + "description": "This structure describes the query messages available in the contract.", + "oneOf": [ + { + "description": "Calculates a new TWAP with updated precision", + "type": "object", + "required": [ + "consult" + ], + "properties": { + "consult": { + "type": "object", + "required": [ + "amount", + "token" + ], + "properties": { + "amount": { + "description": "The amount of tokens for which to compute the token price", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] + }, + "token": { + "description": "The asset for which to compute a new TWAP value", + "allOf": [ + { + "$ref": "#/definitions/AssetInfo" + } + ] + } + } + } + }, + "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 astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"paloma...\") }; 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 + }, + { + "description": "Native token", + "type": "object", + "required": [ + "native_token" + ], + "properties": { + "native_token": { + "type": "object", + "required": [ + "denom" + ], + "properties": { + "denom": { + "type": "string" + } + } + } + }, + "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/contracts/oracle/src/mock_querier.rs b/contracts/oracle/src/mock_querier.rs index 0c51d332..aa1b5e52 100644 --- a/contracts/oracle/src/mock_querier.rs +++ b/contracts/oracle/src/mock_querier.rs @@ -80,7 +80,7 @@ impl WasmMockQuerier { match &request { QueryRequest::Wasm(WasmQuery::Smart { contract_addr, msg }) => { if contract_addr == "factory" { - match from_binary(&msg).unwrap() { + match from_binary(msg).unwrap() { Pair { asset_infos } => SystemResult::Ok( to_binary(&PairInfo { asset_infos, @@ -93,7 +93,7 @@ impl WasmMockQuerier { _ => panic!("DO NOT ENTER HERE"), } } else { - match from_binary(&msg).unwrap() { + match from_binary(msg).unwrap() { CumulativePrices { .. } => { let balance = match self.token_querier.pairs.get(contract_addr) { Some(v) => v, diff --git a/contracts/oracle/src/testing.rs b/contracts/oracle/src/testing.rs index 64164d8f..a87a1810 100644 --- a/contracts/oracle/src/testing.rs +++ b/contracts/oracle/src/testing.rs @@ -16,7 +16,7 @@ fn decimal_overflow() { Uint256::from(price_cumulative_current.wrapping_sub(price_cumulative_last)), time_elapsed, ); - println!("{}", price_average.to_string()); + println!("{}", price_average); println!("{}", price_average * Uint256::from(amount)); } @@ -31,10 +31,10 @@ fn oracle_overflow() { let usdc_token_contract = Addr::unchecked("usdc-token"); let astro_asset_info = AssetInfo::Token { - contract_addr: astro_token_contract.clone(), + contract_addr: astro_token_contract, }; let usdc_asset_info = AssetInfo::Token { - contract_addr: usdc_token_contract.clone(), + contract_addr: usdc_token_contract, }; let astro_asset = Asset { info: astro_asset_info.clone(), @@ -45,7 +45,7 @@ fn oracle_overflow() { amount: Uint128::zero(), }; - let asset = [astro_asset.clone(), usdc_asset.clone()]; + let asset = [astro_asset, usdc_asset]; let instantiate_msg = InstantiateMsg { factory_contract: factory.to_string(), diff --git a/contracts/pair/schema/cumulative_prices_response.json b/contracts/pair/schema/cumulative_prices_response.json new file mode 100644 index 00000000..312ab11e --- /dev/null +++ b/contracts/pair/schema/cumulative_prices_response.json @@ -0,0 +1,131 @@ +{ + "$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", + "price0_cumulative_last", + "price1_cumulative_last", + "total_share" + ], + "properties": { + "assets": { + "description": "The two assets in the pool to query", + "type": "array", + "items": { + "$ref": "#/definitions/Asset" + }, + "maxItems": 2, + "minItems": 2 + }, + "price0_cumulative_last": { + "description": "The last value for the token0 cumulative price", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] + }, + "price1_cumulative_last": { + "description": "The last value for the token1 cumulative price", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] + }, + "total_share": { + "description": "The total amount of LP tokens currently issued", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] + } + }, + "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": { + "title": "Description", + "description": "This enum describes a paloma 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" + } + ] + } + } + }, + "AssetInfo": { + "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"paloma...\") }; 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 + }, + { + "description": "Native token", + "type": "object", + "required": [ + "native_token" + ], + "properties": { + "native_token": { + "type": "object", + "required": [ + "denom" + ], + "properties": { + "denom": { + "type": "string" + } + } + } + }, + "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/contracts/pair/schema/cw20_hook_msg.json b/contracts/pair/schema/cw20_hook_msg.json new file mode 100644 index 00000000..bbbcc1fe --- /dev/null +++ b/contracts/pair/schema/cw20_hook_msg.json @@ -0,0 +1,67 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Cw20HookMsg", + "description": "This structure describes a CW20 hook message.", + "oneOf": [ + { + "description": "Swap a given amount of asset", + "type": "object", + "required": [ + "swap" + ], + "properties": { + "swap": { + "type": "object", + "properties": { + "belief_price": { + "anyOf": [ + { + "$ref": "#/definitions/Decimal" + }, + { + "type": "null" + } + ] + }, + "max_spread": { + "anyOf": [ + { + "$ref": "#/definitions/Decimal" + }, + { + "type": "null" + } + ] + }, + "to": { + "type": [ + "string", + "null" + ] + } + } + } + }, + "additionalProperties": false + }, + { + "description": "Withdraw liquidity from the pool", + "type": "object", + "required": [ + "withdraw_liquidity" + ], + "properties": { + "withdraw_liquidity": { + "type": "object" + } + }, + "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/contracts/pair/schema/execute_msg.json b/contracts/pair/schema/execute_msg.json new file mode 100644 index 00000000..ae349c61 --- /dev/null +++ b/contracts/pair/schema/execute_msg.json @@ -0,0 +1,252 @@ +{ + "$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" + }, + "maxItems": 2, + "minItems": 2 + }, + "auto_stake": { + "description": "Determines whether the LP tokens minted for the user is auto_staked in the Generator contract", + "type": [ + "boolean", + "null" + ] + }, + "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 + }, + { + "description": "Swap performs a swap in the pool", + "type": "object", + "required": [ + "swap" + ], + "properties": { + "swap": { + "type": "object", + "required": [ + "offer_asset" + ], + "properties": { + "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 + }, + { + "description": "Update the pair configuration", + "type": "object", + "required": [ + "update_config" + ], + "properties": { + "update_config": { + "type": "object", + "required": [ + "params" + ], + "properties": { + "params": { + "$ref": "#/definitions/Binary" + } + } + } + }, + "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": { + "title": "Description", + "description": "This enum describes a paloma 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" + } + ] + } + } + }, + "AssetInfo": { + "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"paloma...\") }; 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 + }, + { + "description": "Native token", + "type": "object", + "required": [ + "native_token" + ], + "properties": { + "native_token": { + "type": "object", + "required": [ + "denom" + ], + "properties": { + "denom": { + "type": "string" + } + } + } + }, + "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", + "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" + } + } + }, + "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/contracts/pair/schema/instantiate_msg.json b/contracts/pair/schema/instantiate_msg.json new file mode 100644 index 00000000..238a3fcf --- /dev/null +++ b/contracts/pair/schema/instantiate_msg.json @@ -0,0 +1,100 @@ +{ + "$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 the two assets in the pool", + "type": "array", + "items": { + "$ref": "#/definitions/AssetInfo" + }, + "maxItems": 2, + "minItems": 2 + }, + "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 + } + }, + "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 astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"paloma...\") }; 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 + }, + { + "description": "Native token", + "type": "object", + "required": [ + "native_token" + ], + "properties": { + "native_token": { + "type": "object", + "required": [ + "denom" + ], + "properties": { + "denom": { + "type": "string" + } + } + } + }, + "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", + "type": "string" + } + } +} diff --git a/contracts/pair/schema/migrate_msg.json b/contracts/pair/schema/migrate_msg.json new file mode 100644 index 00000000..18c68d87 --- /dev/null +++ b/contracts/pair/schema/migrate_msg.json @@ -0,0 +1,6 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "MigrateMsg", + "description": "This structure describes a migration message. We currently take no arguments for migrations.", + "type": "object" +} diff --git a/contracts/pair/schema/pair_info.json b/contracts/pair/schema/pair_info.json new file mode 100644 index 00000000..ce5d9c2b --- /dev/null +++ b/contracts/pair/schema/pair_info.json @@ -0,0 +1,144 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "PairInfo", + "description": "This structure stores the main parameters for an Astroport pair", + "type": "object", + "required": [ + "asset_infos", + "contract_addr", + "liquidity_token", + "pair_type" + ], + "properties": { + "asset_infos": { + "description": "Asset information for the two assets in the pool", + "type": "array", + "items": { + "$ref": "#/definitions/AssetInfo" + }, + "maxItems": 2, + "minItems": 2 + }, + "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" + } + ] + } + }, + "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 astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"paloma...\") }; 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 + }, + { + "description": "Native token", + "type": "object", + "required": [ + "native_token" + ], + "properties": { + "native_token": { + "type": "object", + "required": [ + "denom" + ], + "properties": { + "denom": { + "type": "string" + } + } + } + }, + "additionalProperties": false + } + ] + }, + "PairType": { + "description": "This enum describes available pair types. ## Available pool types ``` # use astroport::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 + }, + { + "description": "Stable pair type", + "type": "object", + "required": [ + "stable" + ], + "properties": { + "stable": { + "type": "object" + } + }, + "additionalProperties": false + }, + { + "description": "Custom pair type", + "type": "object", + "required": [ + "custom" + ], + "properties": { + "custom": { + "type": "string" + } + }, + "additionalProperties": false + } + ] + } + } +} diff --git a/contracts/pair/schema/pool_response.json b/contracts/pair/schema/pool_response.json new file mode 100644 index 00000000..3a71ec92 --- /dev/null +++ b/contracts/pair/schema/pool_response.json @@ -0,0 +1,113 @@ +{ + "$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 the two 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" + }, + "maxItems": 2, + "minItems": 2 + }, + "total_share": { + "description": "The total amount of LP tokens currently issued", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] + } + }, + "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": { + "title": "Description", + "description": "This enum describes a paloma 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" + } + ] + } + } + }, + "AssetInfo": { + "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"paloma...\") }; 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 + }, + { + "description": "Native token", + "type": "object", + "required": [ + "native_token" + ], + "properties": { + "native_token": { + "type": "object", + "required": [ + "denom" + ], + "properties": { + "denom": { + "type": "string" + } + } + } + }, + "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/contracts/pair/schema/query_msg.json b/contracts/pair/schema/query_msg.json new file mode 100644 index 00000000..412230e1 --- /dev/null +++ b/contracts/pair/schema/query_msg.json @@ -0,0 +1,219 @@ +{ + "$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 + }, + { + "description": "Returns information about a pool in an object of type [`PoolResponse`].", + "type": "object", + "required": [ + "pool" + ], + "properties": { + "pool": { + "type": "object" + } + }, + "additionalProperties": false + }, + { + "description": "Returns contract configuration settings in a custom [`ConfigResponse`] structure.", + "type": "object", + "required": [ + "config" + ], + "properties": { + "config": { + "type": "object" + } + }, + "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 + }, + { + "description": "Returns information about a swap simulation in a [`SimulationResponse`] object.", + "type": "object", + "required": [ + "simulation" + ], + "properties": { + "simulation": { + "type": "object", + "required": [ + "offer_asset" + ], + "properties": { + "offer_asset": { + "$ref": "#/definitions/Asset" + } + } + } + }, + "additionalProperties": false + }, + { + "description": "Returns information about cumulative prices in a [`CumulativePricesResponse`] object.", + "type": "object", + "required": [ + "reverse_simulation" + ], + "properties": { + "reverse_simulation": { + "type": "object", + "required": [ + "ask_asset" + ], + "properties": { + "ask_asset": { + "$ref": "#/definitions/Asset" + } + } + } + }, + "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 + }, + { + "description": "Returns current D invariant in as a [`u128`] value", + "type": "object", + "required": [ + "query_compute_d" + ], + "properties": { + "query_compute_d": { + "type": "object" + } + }, + "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": { + "title": "Description", + "description": "This enum describes a paloma 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" + } + ] + } + } + }, + "AssetInfo": { + "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"paloma...\") }; 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 + }, + { + "description": "Native token", + "type": "object", + "required": [ + "native_token" + ], + "properties": { + "native_token": { + "type": "object", + "required": [ + "denom" + ], + "properties": { + "denom": { + "type": "string" + } + } + } + }, + "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/contracts/pair/schema/reverse_simulation_response.json b/contracts/pair/schema/reverse_simulation_response.json new file mode 100644 index 00000000..fafa524c --- /dev/null +++ b/contracts/pair/schema/reverse_simulation_response.json @@ -0,0 +1,43 @@ +{ + "$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" + } + ] + } + }, + "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/contracts/pair/schema/simulation_response.json b/contracts/pair/schema/simulation_response.json new file mode 100644 index 00000000..1e23dd25 --- /dev/null +++ b/contracts/pair/schema/simulation_response.json @@ -0,0 +1,43 @@ +{ + "$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" + } + ] + } + }, + "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/contracts/pair/src/mock_querier.rs b/contracts/pair/src/mock_querier.rs index 2882844b..50f4450e 100644 --- a/contracts/pair/src/mock_querier.rs +++ b/contracts/pair/src/mock_querier.rs @@ -133,7 +133,7 @@ impl WasmMockQuerier { } QueryRequest::Wasm(WasmQuery::Smart { contract_addr, msg }) => { if contract_addr == "factory" { - match from_binary(&msg).unwrap() { + match from_binary(msg).unwrap() { FeeInfo { .. } => SystemResult::Ok( to_binary(&FeeInfoResponse { fee_address: Some(Addr::unchecked("fee_address")), @@ -145,7 +145,7 @@ impl WasmMockQuerier { _ => panic!("DO NOT ENTER HERE"), } } else { - match from_binary(&msg).unwrap() { + match from_binary(msg).unwrap() { Cw20QueryMsg::TokenInfo {} => { let balances: &HashMap = match self.token_querier.balances.get(contract_addr) { @@ -166,7 +166,7 @@ impl WasmMockQuerier { name: "mAPPL".to_string(), symbol: "mAPPL".to_string(), decimals: 6, - total_supply: total_supply, + total_supply, }) .into(), ) diff --git a/contracts/pair/src/testing.rs b/contracts/pair/src/testing.rs index 133ba7bc..5ce6f143 100644 --- a/contracts/pair/src/testing.rs +++ b/contracts/pair/src/testing.rs @@ -43,7 +43,7 @@ fn store_liquidity_token(deps: DepsMut, msg_id: u64, contrac }), }; - let _res = reply(deps, mock_env(), reply_msg.clone()).unwrap(); + let _res = reply(deps, mock_env(), reply_msg).unwrap(); } #[test] @@ -71,9 +71,8 @@ fn proper_initialization() { 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(); + let res = instantiate(deps.as_mut(), mock_env(), info, msg).unwrap(); assert_eq!( res.messages, vec![SubMsg { @@ -124,7 +123,7 @@ fn proper_initialization() { fn provide_liquidity() { let mut deps = mock_dependencies(&[Coin { denom: "uusd".to_string(), - amount: Uint128::new(200_000000000000000000u128), + amount: Uint128::new(200_000000000000000000_u128), }]); deps.querier.with_token_balances(&[ @@ -152,10 +151,9 @@ fn provide_liquidity() { init_params: None, }; - 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 _res = instantiate(deps.as_mut(), mock_env(), info, msg).unwrap(); // Store liquidity token store_liquidity_token(deps.as_mut(), 1, "liquidity0000".to_string()); @@ -167,7 +165,7 @@ fn provide_liquidity() { info: AssetInfo::Token { contract_addr: Addr::unchecked("asset0000"), }, - amount: Uint128::from(100_000000000000000000u128), + amount: Uint128::from(100_000000000000000000_u128), }, Asset { info: AssetInfo::NativeToken { @@ -181,7 +179,6 @@ fn provide_liquidity() { receiver: None, }; - let env = mock_env(); let info = mock_info( "addr0000", &[Coin { @@ -189,7 +186,7 @@ fn provide_liquidity() { amount: Uint128::from(100_000000000000000000u128), }], ); - let res = execute(deps.as_mut(), env.clone().clone(), info, msg).unwrap(); + let res = execute(deps.as_mut(), mock_env(), 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!( @@ -277,7 +274,8 @@ fn provide_liquidity() { receiver: None, }; - let env = mock_env_with_block_time(env.block.time.seconds() + 1000); + let mut env_block_time_seconds = mock_env().block.time.seconds() + 1000; + let env = mock_env_with_block_time(env_block_time_seconds); let info = mock_info( "addr0000", &[Coin { @@ -287,7 +285,7 @@ fn provide_liquidity() { ); // Only accept 100, then 50 share will be generated with 100 * (100 / 200) - let res: Response = execute(deps.as_mut(), env.clone(), info, msg).unwrap(); + let res: Response = execute(deps.as_mut(), env, 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!( @@ -349,7 +347,6 @@ fn provide_liquidity() { receiver: None, }; - let env = mock_env(); let info = mock_info( "addr0000", &[Coin { @@ -357,7 +354,7 @@ fn provide_liquidity() { amount: Uint128::from(100_000000000000000000u128), }], ); - let res = execute(deps.as_mut(), env.clone(), info, msg).unwrap_err(); + let res = execute(deps.as_mut(), mock_env(), info, msg).unwrap_err(); match res { ContractError::Std(StdError::GenericErr { msg, .. }) => assert_eq!( msg, @@ -413,7 +410,8 @@ fn provide_liquidity() { receiver: None, }; - let env = mock_env_with_block_time(env.block.time.seconds() + 1000); + env_block_time_seconds += 1000; + let env = mock_env_with_block_time(env_block_time_seconds); let info = mock_info( "addr0001", &[Coin { @@ -421,7 +419,7 @@ fn provide_liquidity() { amount: Uint128::from(100_000000000000000000u128), }], ); - let res = execute(deps.as_mut(), env.clone(), info, msg).unwrap_err(); + let res = execute(deps.as_mut(), env, info, msg).unwrap_err(); assert_eq!(res, ContractError::MaxSlippageAssertion {}); // Initialize token balance to 1:1 @@ -454,7 +452,8 @@ fn provide_liquidity() { receiver: None, }; - let env = mock_env_with_block_time(env.block.time.seconds() + 1000); + env_block_time_seconds += 1000; + let env = mock_env_with_block_time(env_block_time_seconds); let info = mock_info( "addr0001", &[Coin { @@ -462,7 +461,7 @@ fn provide_liquidity() { amount: Uint128::from(98_000000000000000000u128), }], ); - let res = execute(deps.as_mut(), env.clone(), info, msg).unwrap_err(); + let res = execute(deps.as_mut(), env, info, msg).unwrap_err(); assert_eq!(res, ContractError::MaxSlippageAssertion {}); // Initialize token amount with a 1:1 ratio @@ -495,7 +494,8 @@ fn provide_liquidity() { receiver: None, }; - let env = mock_env_with_block_time(env.block.time.seconds() + 1000); + env_block_time_seconds += 1000; + let env = mock_env_with_block_time(env_block_time_seconds); let info = mock_info( "addr0001", &[Coin { @@ -503,7 +503,7 @@ fn provide_liquidity() { amount: Uint128::from(100_000000000000000000u128), }], ); - let _res = execute(deps.as_mut(), env.clone(), info, msg).unwrap(); + let _res = execute(deps.as_mut(), env, info, msg).unwrap(); // Initialize token balance to 1:1 deps.querier.with_balance(&[( @@ -535,7 +535,8 @@ fn provide_liquidity() { receiver: None, }; - let env = mock_env_with_block_time(env.block.time.seconds() + 1000); + env_block_time_seconds += 1000; + let env = mock_env_with_block_time(env_block_time_seconds); let info = mock_info( "addr0001", &[Coin { @@ -583,10 +584,9 @@ fn withdraw_liquidity() { init_params: None, }; - 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 _res = instantiate(deps.as_mut(), mock_env(), info, msg).unwrap(); // Store liquidity token store_liquidity_token(deps.as_mut(), 1, "liquidity0000".to_string()); @@ -598,9 +598,8 @@ fn withdraw_liquidity() { amount: Uint128::new(100u128), }); - let env = mock_env(); let info = mock_info("liquidity0000", &[]); - let res = execute(deps.as_mut(), env, info, msg).unwrap(); + let res = execute(deps.as_mut(), mock_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"); @@ -709,10 +708,9 @@ fn try_native_to_token() { init_params: None, }; - 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 _res = instantiate(deps.as_mut(), mock_env(), info, msg).unwrap(); // Store liquidity token store_liquidity_token(deps.as_mut(), 1, "liquidity0000".to_string()); @@ -790,24 +788,21 @@ fn try_native_to_token() { }, ) .unwrap(); - assert_eq!( + assert!( (offer_amount.u128() as i128 - reverse_simulation_res.offer_amount.u128() as i128).abs() - < 5i128, - true + < 5i128 ); - assert_eq!( + assert!( (expected_commission_amount.u128() as i128 - reverse_simulation_res.commission_amount.u128() as i128) .abs() - < 5i128, - true + < 5i128 ); - assert_eq!( + assert!( (expected_spread_amount.u128() as i128 - reverse_simulation_res.spread_amount.u128() as i128) .abs() - < 5i128, - true + < 5i128 ); assert_eq!( @@ -833,7 +828,7 @@ fn try_native_to_token() { contract_addr: String::from("asset0000"), msg: to_binary(&Cw20ExecuteMsg::Transfer { recipient: String::from("addr0000"), - amount: Uint128::from(expected_return_amount), + amount: expected_return_amount, }) .unwrap(), funds: vec![], @@ -890,10 +885,9 @@ fn try_token_to_native() { init_params: None, }; - 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 _res = instantiate(deps.as_mut(), mock_env(), info, msg).unwrap(); // Store liquidity token store_liquidity_token(deps.as_mut(), 1, "liquidity0000".to_string()); @@ -990,24 +984,21 @@ fn try_token_to_native() { }, ) .unwrap(); - assert_eq!( + assert!( (offer_amount.u128() as i128 - reverse_simulation_res.offer_amount.u128() as i128).abs() - < 5i128, - true + < 5i128 ); - assert_eq!( + assert!( (expected_commission_amount.u128() as i128 - reverse_simulation_res.commission_amount.u128() as i128) .abs() - < 5i128, - true + < 5i128 ); - assert_eq!( + assert!( (expected_spread_amount.u128() as i128 - reverse_simulation_res.spread_amount.u128() as i128) .abs() - < 5i128, - true + < 5i128 ); assert_eq!( @@ -1037,8 +1028,7 @@ fn try_token_to_native() { .checked_sub(expected_tax_amount) .unwrap(), }], - }) - .into(), + }), id: 0, gas_limit: None, reply_on: ReplyOn::Never, @@ -1113,7 +1103,7 @@ fn test_deduct() { &[(&"uusd".to_string(), &Uint128::from(1000000u128))], ); - let amount = Uint128::new(1000_000_000u128); + let amount = Uint128::new(1_000_000_000u128); let expected_after_amount = std::cmp::max( amount.checked_sub(amount * tax_rate).unwrap(), amount.checked_sub(tax_cap).unwrap(), @@ -1166,10 +1156,9 @@ fn test_query_pool() { init_params: None, }; - 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 _res = instantiate(deps.as_mut(), mock_env(), info, msg).unwrap(); // Store liquidity token store_liquidity_token(deps.as_mut(), 1, "liquidity0000".to_string()); @@ -1231,10 +1220,9 @@ fn test_query_share() { init_params: None, }; - 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 _res = instantiate(deps.as_mut(), mock_env(), info, msg).unwrap(); // Store liquidity token store_liquidity_token(deps.as_mut(), 1, "liquidity0000".to_string()); @@ -1287,7 +1275,7 @@ fn test_accumulate_prices() { Case { block_time: 1000, block_time_last: 1000, - last0: 1 * price_precision, + last0: price_precision, last1: 2 * price_precision, x_amount: 250, y_amount: 500, @@ -1393,7 +1381,7 @@ proptest! { fn compute_swap_overflow_test( offer_pool in 1_000_000..9_000_000_000_000_000_000u128, ask_pool in 1_000_000..9_000_000_000_000_000_000u128, - offer_amount in 1..100_000_000000u128, + offer_amount in 1..100_000_000_000_u128, ) { let offer_pool = Uint128::from(offer_pool); diff --git a/contracts/pair_anchor/schema/cumulative_prices_response.json b/contracts/pair_anchor/schema/cumulative_prices_response.json new file mode 100644 index 00000000..312ab11e --- /dev/null +++ b/contracts/pair_anchor/schema/cumulative_prices_response.json @@ -0,0 +1,131 @@ +{ + "$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", + "price0_cumulative_last", + "price1_cumulative_last", + "total_share" + ], + "properties": { + "assets": { + "description": "The two assets in the pool to query", + "type": "array", + "items": { + "$ref": "#/definitions/Asset" + }, + "maxItems": 2, + "minItems": 2 + }, + "price0_cumulative_last": { + "description": "The last value for the token0 cumulative price", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] + }, + "price1_cumulative_last": { + "description": "The last value for the token1 cumulative price", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] + }, + "total_share": { + "description": "The total amount of LP tokens currently issued", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] + } + }, + "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": { + "title": "Description", + "description": "This enum describes a paloma 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" + } + ] + } + } + }, + "AssetInfo": { + "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"paloma...\") }; 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 + }, + { + "description": "Native token", + "type": "object", + "required": [ + "native_token" + ], + "properties": { + "native_token": { + "type": "object", + "required": [ + "denom" + ], + "properties": { + "denom": { + "type": "string" + } + } + } + }, + "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/contracts/pair_anchor/schema/cw20_hook_msg.json b/contracts/pair_anchor/schema/cw20_hook_msg.json new file mode 100644 index 00000000..bbbcc1fe --- /dev/null +++ b/contracts/pair_anchor/schema/cw20_hook_msg.json @@ -0,0 +1,67 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Cw20HookMsg", + "description": "This structure describes a CW20 hook message.", + "oneOf": [ + { + "description": "Swap a given amount of asset", + "type": "object", + "required": [ + "swap" + ], + "properties": { + "swap": { + "type": "object", + "properties": { + "belief_price": { + "anyOf": [ + { + "$ref": "#/definitions/Decimal" + }, + { + "type": "null" + } + ] + }, + "max_spread": { + "anyOf": [ + { + "$ref": "#/definitions/Decimal" + }, + { + "type": "null" + } + ] + }, + "to": { + "type": [ + "string", + "null" + ] + } + } + } + }, + "additionalProperties": false + }, + { + "description": "Withdraw liquidity from the pool", + "type": "object", + "required": [ + "withdraw_liquidity" + ], + "properties": { + "withdraw_liquidity": { + "type": "object" + } + }, + "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/contracts/pair_anchor/schema/execute_msg.json b/contracts/pair_anchor/schema/execute_msg.json new file mode 100644 index 00000000..de47c5e9 --- /dev/null +++ b/contracts/pair_anchor/schema/execute_msg.json @@ -0,0 +1,319 @@ +{ + "$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" + }, + "maxItems": 2, + "minItems": 2 + }, + "auto_stake": { + "description": "Determines whether the LP tokens minted for the user is auto_staked in the Generator contract", + "type": [ + "boolean", + "null" + ] + }, + "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 + }, + { + "description": "Swap performs a swap in the pool", + "type": "object", + "required": [ + "swap" + ], + "properties": { + "swap": { + "type": "object", + "required": [ + "offer_asset" + ], + "properties": { + "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 + }, + { + "description": "Update the pair configuration", + "type": "object", + "required": [ + "update_config" + ], + "properties": { + "update_config": { + "type": "object", + "required": [ + "params" + ], + "properties": { + "params": { + "$ref": "#/definitions/Binary" + } + } + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "assert_and_send" + ], + "properties": { + "assert_and_send": { + "type": "object", + "required": [ + "ask_asset_info", + "offer_asset", + "receiver", + "sender" + ], + "properties": { + "ask_asset_info": { + "description": "Information about an asset stored in a [`AssetInfo`] struct", + "allOf": [ + { + "$ref": "#/definitions/AssetInfo" + } + ] + }, + "belief_price": { + "anyOf": [ + { + "$ref": "#/definitions/Decimal" + }, + { + "type": "null" + } + ] + }, + "max_spread": { + "anyOf": [ + { + "$ref": "#/definitions/Decimal" + }, + { + "type": "null" + } + ] + }, + "offer_asset": { + "$ref": "#/definitions/Asset" + }, + "receiver": { + "description": "Receiver who should receive the funds", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + }, + "sender": { + "description": "Sender who initiated the transaction", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + } + } + } + }, + "additionalProperties": false + } + ], + "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + }, + "Asset": { + "title": "Description", + "description": "This enum describes a paloma 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" + } + ] + } + } + }, + "AssetInfo": { + "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"paloma...\") }; 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 + }, + { + "description": "Native token", + "type": "object", + "required": [ + "native_token" + ], + "properties": { + "native_token": { + "type": "object", + "required": [ + "denom" + ], + "properties": { + "denom": { + "type": "string" + } + } + } + }, + "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", + "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" + } + } + }, + "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/contracts/pair_anchor/schema/instantiate_msg.json b/contracts/pair_anchor/schema/instantiate_msg.json new file mode 100644 index 00000000..238a3fcf --- /dev/null +++ b/contracts/pair_anchor/schema/instantiate_msg.json @@ -0,0 +1,100 @@ +{ + "$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 the two assets in the pool", + "type": "array", + "items": { + "$ref": "#/definitions/AssetInfo" + }, + "maxItems": 2, + "minItems": 2 + }, + "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 + } + }, + "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 astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"paloma...\") }; 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 + }, + { + "description": "Native token", + "type": "object", + "required": [ + "native_token" + ], + "properties": { + "native_token": { + "type": "object", + "required": [ + "denom" + ], + "properties": { + "denom": { + "type": "string" + } + } + } + }, + "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", + "type": "string" + } + } +} diff --git a/contracts/pair_anchor/schema/migrate_msg.json b/contracts/pair_anchor/schema/migrate_msg.json new file mode 100644 index 00000000..18c68d87 --- /dev/null +++ b/contracts/pair_anchor/schema/migrate_msg.json @@ -0,0 +1,6 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "MigrateMsg", + "description": "This structure describes a migration message. We currently take no arguments for migrations.", + "type": "object" +} diff --git a/contracts/pair_anchor/schema/pair_info.json b/contracts/pair_anchor/schema/pair_info.json new file mode 100644 index 00000000..ce5d9c2b --- /dev/null +++ b/contracts/pair_anchor/schema/pair_info.json @@ -0,0 +1,144 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "PairInfo", + "description": "This structure stores the main parameters for an Astroport pair", + "type": "object", + "required": [ + "asset_infos", + "contract_addr", + "liquidity_token", + "pair_type" + ], + "properties": { + "asset_infos": { + "description": "Asset information for the two assets in the pool", + "type": "array", + "items": { + "$ref": "#/definitions/AssetInfo" + }, + "maxItems": 2, + "minItems": 2 + }, + "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" + } + ] + } + }, + "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 astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"paloma...\") }; 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 + }, + { + "description": "Native token", + "type": "object", + "required": [ + "native_token" + ], + "properties": { + "native_token": { + "type": "object", + "required": [ + "denom" + ], + "properties": { + "denom": { + "type": "string" + } + } + } + }, + "additionalProperties": false + } + ] + }, + "PairType": { + "description": "This enum describes available pair types. ## Available pool types ``` # use astroport::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 + }, + { + "description": "Stable pair type", + "type": "object", + "required": [ + "stable" + ], + "properties": { + "stable": { + "type": "object" + } + }, + "additionalProperties": false + }, + { + "description": "Custom pair type", + "type": "object", + "required": [ + "custom" + ], + "properties": { + "custom": { + "type": "string" + } + }, + "additionalProperties": false + } + ] + } + } +} diff --git a/contracts/pair_anchor/schema/pool_response.json b/contracts/pair_anchor/schema/pool_response.json new file mode 100644 index 00000000..3a71ec92 --- /dev/null +++ b/contracts/pair_anchor/schema/pool_response.json @@ -0,0 +1,113 @@ +{ + "$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 the two 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" + }, + "maxItems": 2, + "minItems": 2 + }, + "total_share": { + "description": "The total amount of LP tokens currently issued", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] + } + }, + "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": { + "title": "Description", + "description": "This enum describes a paloma 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" + } + ] + } + } + }, + "AssetInfo": { + "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"paloma...\") }; 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 + }, + { + "description": "Native token", + "type": "object", + "required": [ + "native_token" + ], + "properties": { + "native_token": { + "type": "object", + "required": [ + "denom" + ], + "properties": { + "denom": { + "type": "string" + } + } + } + }, + "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/contracts/pair_anchor/schema/query_msg.json b/contracts/pair_anchor/schema/query_msg.json new file mode 100644 index 00000000..44299466 --- /dev/null +++ b/contracts/pair_anchor/schema/query_msg.json @@ -0,0 +1,206 @@ +{ + "$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 + }, + { + "description": "Returns information about a pool in an object of type [`PoolResponse`].", + "type": "object", + "required": [ + "pool" + ], + "properties": { + "pool": { + "type": "object" + } + }, + "additionalProperties": false + }, + { + "description": "Returns contract configuration settings in a custom [`ConfigResponse`] structure.", + "type": "object", + "required": [ + "config" + ], + "properties": { + "config": { + "type": "object" + } + }, + "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 + }, + { + "description": "Returns information about a swap simulation in a [`SimulationResponse`] object.", + "type": "object", + "required": [ + "simulation" + ], + "properties": { + "simulation": { + "type": "object", + "required": [ + "offer_asset" + ], + "properties": { + "offer_asset": { + "$ref": "#/definitions/Asset" + } + } + } + }, + "additionalProperties": false + }, + { + "description": "Returns information about cumulative prices in a [`CumulativePricesResponse`] object.", + "type": "object", + "required": [ + "reverse_simulation" + ], + "properties": { + "reverse_simulation": { + "type": "object", + "required": [ + "ask_asset" + ], + "properties": { + "ask_asset": { + "$ref": "#/definitions/Asset" + } + } + } + }, + "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 + } + ], + "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": { + "title": "Description", + "description": "This enum describes a paloma 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" + } + ] + } + } + }, + "AssetInfo": { + "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"paloma...\") }; 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 + }, + { + "description": "Native token", + "type": "object", + "required": [ + "native_token" + ], + "properties": { + "native_token": { + "type": "object", + "required": [ + "denom" + ], + "properties": { + "denom": { + "type": "string" + } + } + } + }, + "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/contracts/pair_anchor/schema/reverse_simulation_response.json b/contracts/pair_anchor/schema/reverse_simulation_response.json new file mode 100644 index 00000000..fafa524c --- /dev/null +++ b/contracts/pair_anchor/schema/reverse_simulation_response.json @@ -0,0 +1,43 @@ +{ + "$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" + } + ] + } + }, + "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/contracts/pair_anchor/schema/simulation_response.json b/contracts/pair_anchor/schema/simulation_response.json new file mode 100644 index 00000000..1e23dd25 --- /dev/null +++ b/contracts/pair_anchor/schema/simulation_response.json @@ -0,0 +1,43 @@ +{ + "$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" + } + ] + } + }, + "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/contracts/pair_anchor/src/mock_querier.rs b/contracts/pair_anchor/src/mock_querier.rs index 49227aa7..4154e199 100644 --- a/contracts/pair_anchor/src/mock_querier.rs +++ b/contracts/pair_anchor/src/mock_querier.rs @@ -135,7 +135,7 @@ impl WasmMockQuerier { } QueryRequest::Wasm(WasmQuery::Smart { contract_addr, msg }) => { if contract_addr == "factory" { - match from_binary(&msg).unwrap() { + match from_binary(msg).unwrap() { QueryMsg::FeeInfo { .. } => SystemResult::Ok( to_binary(&FeeInfoResponse { fee_address: Some(Addr::unchecked("fee_address")), @@ -147,7 +147,7 @@ impl WasmMockQuerier { _ => panic!("DO NOT ENTER HERE"), } } else if contract_addr == "anchor" { - match from_binary(&msg).unwrap() { + match from_binary(msg).unwrap() { AnchorQueryMsg::EpochState { .. } => SystemResult::Ok( to_binary(&EpochStateResponse { exchange_rate: Decimal256::from_str("1.216736524026807943") @@ -159,7 +159,7 @@ impl WasmMockQuerier { _ => panic!("DO NOT ENTER HERE"), } } else { - match from_binary(&msg).unwrap() { + match from_binary(msg).unwrap() { Cw20QueryMsg::TokenInfo {} => { let balances: &HashMap = match self.token_querier.balances.get(contract_addr) { @@ -180,7 +180,7 @@ impl WasmMockQuerier { name: "mAPPL".to_string(), symbol: "mAPPL".to_string(), decimals: 6, - total_supply: total_supply, + total_supply, }) .into(), ) diff --git a/contracts/pair_anchor/src/testing.rs b/contracts/pair_anchor/src/testing.rs index a787975f..5114566e 100644 --- a/contracts/pair_anchor/src/testing.rs +++ b/contracts/pair_anchor/src/testing.rs @@ -22,7 +22,7 @@ const MOCK_ANCHOR_ADDR: &str = "anchor"; const MOCK_ANCHOR_TOKEN: &str = "addr1aust"; fn create_init_params() -> Option { - return Some(to_binary(&MOCK_ANCHOR_ADDR.to_string()).unwrap()); + Some(to_binary(&MOCK_ANCHOR_ADDR.to_string()).unwrap()) } #[test] @@ -38,9 +38,8 @@ fn proper_initialization() { 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(); + let res = instantiate(deps.as_mut(), mock_env(), info, msg).unwrap(); assert_eq!(res.messages.len(), 0); // It worked, let's query the state @@ -73,15 +72,14 @@ fn update_config() { let msg = get_instantiate_message(); - 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.clone(), msg).unwrap(); + instantiate(deps.as_mut(), mock_env(), info.clone(), msg).unwrap(); // We can not update config for a virtual pool let res = execute( deps.as_mut(), - env, + mock_env(), info, ExecuteMsg::UpdateConfig { params: Default::default(), @@ -105,10 +103,9 @@ fn provide_liquidity() { let msg = get_instantiate_message(); - 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, info, msg).unwrap(); + instantiate(deps.as_mut(), mock_env(), info, msg).unwrap(); // We can not provide liquidity for a virtual pool let msg = ExecuteMsg::ProvideLiquidity { @@ -131,7 +128,6 @@ fn provide_liquidity() { receiver: None, }; - let env = mock_env(); let info = mock_info( "addr0000", &[Coin { @@ -139,7 +135,7 @@ fn provide_liquidity() { amount: Uint128::from(100_000000000000000000u128), }], ); - let res = execute(deps.as_mut(), env.clone().clone(), info, msg); + let res = execute(deps.as_mut(), mock_env(), info, msg); assert_eq!(res, Err(ContractError::NonSupported {})) } @@ -162,10 +158,9 @@ fn withdraw_liquidity() { let msg = get_instantiate_message(); - 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, info, msg).unwrap(); + instantiate(deps.as_mut(), mock_env(), info, msg).unwrap(); // Withdraw liquidity let msg = ExecuteMsg::Receive(Cw20ReceiveMsg { @@ -174,9 +169,8 @@ fn withdraw_liquidity() { amount: Uint128::new(100u128), }); - let env = mock_env(); let info = mock_info("liquidity0000", &[]); - let res = execute(deps.as_mut(), env, info, msg); + let res = execute(deps.as_mut(), mock_env(), info, msg); assert_eq!(res, Err(ContractError::NonSupported {})) } @@ -206,7 +200,7 @@ fn try_native_to_token() { let env = mock_env(); let info = mock_info("addr0000", &[]); - let info_contract = mock_info(env.contract.address.clone().as_str(), &[]); + let info_contract = mock_info(env.contract.address.as_str(), &[]); // we can just call .unwrap() to assert this was a success instantiate(deps.as_mut(), env, info, msg).unwrap(); @@ -283,24 +277,21 @@ fn try_native_to_token() { ) .unwrap(); - assert_eq!( + assert!( (offer_amount.u128() as i128 - reverse_simulation_res.offer_amount.u128() as i128).abs() - < 5i128, - true + < 5i128 ); - assert_eq!( + assert!( (expected_commission_amount.u128() as i128 - reverse_simulation_res.commission_amount.u128() as i128) .abs() - < 5i128, - true + < 5i128 ); - assert_eq!( + assert!( (expected_spread_amount.u128() as i128 - reverse_simulation_res.spread_amount.u128() as i128) .abs() - < 5i128, - true + < 5i128 ); let first_msg = res.messages.get(0).unwrap(); @@ -364,7 +355,7 @@ fn try_native_to_token() { &[(&String::from(MOCK_CONTRACT_ADDR), &expected_return_amount)], )]); - let second_response = execute(deps.as_mut(), env.clone(), info_contract, sub_msg).unwrap(); + let second_response = execute(deps.as_mut(), env, info_contract, sub_msg).unwrap(); let msg_transfer = second_response.messages.get(0).expect("no message"); assert_eq!( @@ -390,7 +381,7 @@ fn try_native_to_token() { contract_addr: String::from(MOCK_ANCHOR_TOKEN), msg: to_binary(&Cw20ExecuteMsg::Transfer { recipient: String::from("addr0000"), - amount: Uint128::from(expected_return_amount), + amount: expected_return_amount, }) .unwrap(), funds: vec![], @@ -428,10 +419,9 @@ fn try_token_to_native() { let msg = get_instantiate_message(); - 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, info, msg).unwrap(); + instantiate(deps.as_mut(), mock_env(), info, msg).unwrap(); // Unauthorized access; can not execute swap directy for token swap let msg = ExecuteMsg::Swap { @@ -448,7 +438,7 @@ fn try_token_to_native() { let sender = "addr0000"; let env = mock_env_with_block_time(1000); let info = mock_info(sender, &[]); - let res = execute(deps.as_mut(), env.clone(), info.clone(), msg).unwrap_err(); + let res = execute(deps.as_mut(), env, info, msg).unwrap_err(); assert_eq!(res, ContractError::Unauthorized {}); // Normal sell @@ -466,7 +456,7 @@ fn try_token_to_native() { let info = mock_info(MOCK_ANCHOR_TOKEN, &[]); let info_contract = mock_info(env.contract.address.as_str(), &[]); - let res = execute(deps.as_mut(), env.clone(), info.clone(), msg).unwrap(); + let res = execute(deps.as_mut(), env.clone(), info, msg).unwrap(); // Current price is 1.216736524026807943 // so ret_amount = 1_500_000_000 * 1.216736524026807943 @@ -575,7 +565,7 @@ fn try_token_to_native() { }) => { assert_eq!(contract_addr, MOCK_CONTRACT_ADDR); assert!(funds.is_empty()); - sub_msg = from_binary(&msg).unwrap(); + sub_msg = from_binary(msg).unwrap(); assert_eq!( sub_msg, @@ -610,8 +600,7 @@ fn try_token_to_native() { }], )]); - let second_response = - execute(deps.as_mut(), env.clone().clone(), info_contract, sub_msg).unwrap(); + let second_response = execute(deps.as_mut(), env, info_contract, sub_msg).unwrap(); let msg_transfer = second_response.messages.get(0).expect("no message"); assert_eq!( @@ -642,8 +631,7 @@ fn try_token_to_native() { .checked_sub(expected_tax_amount) .unwrap(), }], - }) - .into(), + }), id: 0, gas_limit: None, reply_on: ReplyOn::Never, @@ -705,7 +693,7 @@ fn test_deduct() { &[(&"uusd".to_string(), &Uint128::from(1000000u128))], ); - let amount = Uint128::new(1000_000_000u128); + let amount = Uint128::new(1_000_000_000_u128); let expected_after_amount = std::cmp::max( amount.checked_sub(amount * tax_rate).unwrap(), amount.checked_sub(tax_cap).unwrap(), @@ -972,8 +960,7 @@ fn test_sending_aust_balance_to_maker() { &[(&String::from(MOCK_CONTRACT_ADDR), &expected_return_amount)], )]); - let second_response = - execute(deps.as_mut(), env.clone().clone(), info_contract, sub_msg).unwrap(); + let second_response = execute(deps.as_mut(), env, info_contract, sub_msg).unwrap(); let msg_transfer = second_response.messages.get(0).expect("no message"); assert_eq!( @@ -1062,7 +1049,7 @@ fn test_sending_ust_balance_to_maker() { let sender = "addr0000"; let env = mock_env_with_block_time(1000); let info = mock_info(sender, &[]); - let res = execute(deps.as_mut(), env.clone(), info.clone(), msg).unwrap_err(); + let res = execute(deps.as_mut(), env, info, msg).unwrap_err(); assert_eq!(res, ContractError::Unauthorized {}); // Normal sell @@ -1078,9 +1065,9 @@ fn test_sending_ust_balance_to_maker() { }); let env = mock_env_with_block_time(1000); let info = mock_info(MOCK_ANCHOR_TOKEN, &[]); - let info_contract = mock_info(env.contract.address.clone().as_str(), &[]); + let info_contract = mock_info(env.contract.address.as_str(), &[]); - let res = execute(deps.as_mut(), env.clone(), info.clone(), msg).unwrap(); + let res = execute(deps.as_mut(), env.clone(), info, msg).unwrap(); // Current price is 1.216736524026807943 // so ret_amount = 1_500_000_000 * 1.216736524026807943 @@ -1134,24 +1121,21 @@ fn test_sending_ust_balance_to_maker() { }, ) .unwrap(); - assert_eq!( + assert!( (offer_amount.u128() as i128 - reverse_simulation_res.offer_amount.u128() as i128).abs() - < 5i128, - true + < 5i128 ); - assert_eq!( + assert!( (expected_commission_amount.u128() as i128 - reverse_simulation_res.commission_amount.u128() as i128) .abs() - < 5i128, - true + < 5i128 ); - assert_eq!( + assert!( (expected_spread_amount.u128() as i128 - reverse_simulation_res.spread_amount.u128() as i128) .abs() - < 5i128, - true + < 5i128 ); // checks that the existing assets (uusd) are moved to the maker contract @@ -1246,8 +1230,7 @@ fn test_sending_ust_balance_to_maker() { }], )]); - let second_response = - execute(deps.as_mut(), env.clone().clone(), info_contract, sub_msg).unwrap(); + let second_response = execute(deps.as_mut(), env, info_contract, sub_msg).unwrap(); let msg_transfer = second_response.messages.get(0).expect("no message"); assert_eq!( @@ -1277,8 +1260,7 @@ fn test_sending_ust_balance_to_maker() { .checked_sub(expected_tax_amount) .unwrap(), }], - }) - .into(), + }), id: 0, gas_limit: None, reply_on: ReplyOn::Never, diff --git a/contracts/pair_stable/schema/cumulative_prices_response.json b/contracts/pair_stable/schema/cumulative_prices_response.json new file mode 100644 index 00000000..312ab11e --- /dev/null +++ b/contracts/pair_stable/schema/cumulative_prices_response.json @@ -0,0 +1,131 @@ +{ + "$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", + "price0_cumulative_last", + "price1_cumulative_last", + "total_share" + ], + "properties": { + "assets": { + "description": "The two assets in the pool to query", + "type": "array", + "items": { + "$ref": "#/definitions/Asset" + }, + "maxItems": 2, + "minItems": 2 + }, + "price0_cumulative_last": { + "description": "The last value for the token0 cumulative price", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] + }, + "price1_cumulative_last": { + "description": "The last value for the token1 cumulative price", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] + }, + "total_share": { + "description": "The total amount of LP tokens currently issued", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] + } + }, + "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": { + "title": "Description", + "description": "This enum describes a paloma 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" + } + ] + } + } + }, + "AssetInfo": { + "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"paloma...\") }; 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 + }, + { + "description": "Native token", + "type": "object", + "required": [ + "native_token" + ], + "properties": { + "native_token": { + "type": "object", + "required": [ + "denom" + ], + "properties": { + "denom": { + "type": "string" + } + } + } + }, + "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/contracts/pair_stable/schema/cw20_hook_msg.json b/contracts/pair_stable/schema/cw20_hook_msg.json new file mode 100644 index 00000000..bbbcc1fe --- /dev/null +++ b/contracts/pair_stable/schema/cw20_hook_msg.json @@ -0,0 +1,67 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Cw20HookMsg", + "description": "This structure describes a CW20 hook message.", + "oneOf": [ + { + "description": "Swap a given amount of asset", + "type": "object", + "required": [ + "swap" + ], + "properties": { + "swap": { + "type": "object", + "properties": { + "belief_price": { + "anyOf": [ + { + "$ref": "#/definitions/Decimal" + }, + { + "type": "null" + } + ] + }, + "max_spread": { + "anyOf": [ + { + "$ref": "#/definitions/Decimal" + }, + { + "type": "null" + } + ] + }, + "to": { + "type": [ + "string", + "null" + ] + } + } + } + }, + "additionalProperties": false + }, + { + "description": "Withdraw liquidity from the pool", + "type": "object", + "required": [ + "withdraw_liquidity" + ], + "properties": { + "withdraw_liquidity": { + "type": "object" + } + }, + "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/contracts/pair_stable/schema/execute_msg.json b/contracts/pair_stable/schema/execute_msg.json new file mode 100644 index 00000000..ae349c61 --- /dev/null +++ b/contracts/pair_stable/schema/execute_msg.json @@ -0,0 +1,252 @@ +{ + "$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" + }, + "maxItems": 2, + "minItems": 2 + }, + "auto_stake": { + "description": "Determines whether the LP tokens minted for the user is auto_staked in the Generator contract", + "type": [ + "boolean", + "null" + ] + }, + "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 + }, + { + "description": "Swap performs a swap in the pool", + "type": "object", + "required": [ + "swap" + ], + "properties": { + "swap": { + "type": "object", + "required": [ + "offer_asset" + ], + "properties": { + "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 + }, + { + "description": "Update the pair configuration", + "type": "object", + "required": [ + "update_config" + ], + "properties": { + "update_config": { + "type": "object", + "required": [ + "params" + ], + "properties": { + "params": { + "$ref": "#/definitions/Binary" + } + } + } + }, + "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": { + "title": "Description", + "description": "This enum describes a paloma 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" + } + ] + } + } + }, + "AssetInfo": { + "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"paloma...\") }; 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 + }, + { + "description": "Native token", + "type": "object", + "required": [ + "native_token" + ], + "properties": { + "native_token": { + "type": "object", + "required": [ + "denom" + ], + "properties": { + "denom": { + "type": "string" + } + } + } + }, + "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", + "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" + } + } + }, + "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/contracts/pair_stable/schema/instantiate_msg.json b/contracts/pair_stable/schema/instantiate_msg.json new file mode 100644 index 00000000..238a3fcf --- /dev/null +++ b/contracts/pair_stable/schema/instantiate_msg.json @@ -0,0 +1,100 @@ +{ + "$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 the two assets in the pool", + "type": "array", + "items": { + "$ref": "#/definitions/AssetInfo" + }, + "maxItems": 2, + "minItems": 2 + }, + "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 + } + }, + "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 astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"paloma...\") }; 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 + }, + { + "description": "Native token", + "type": "object", + "required": [ + "native_token" + ], + "properties": { + "native_token": { + "type": "object", + "required": [ + "denom" + ], + "properties": { + "denom": { + "type": "string" + } + } + } + }, + "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", + "type": "string" + } + } +} diff --git a/contracts/pair_stable/schema/migrate_msg.json b/contracts/pair_stable/schema/migrate_msg.json new file mode 100644 index 00000000..18c68d87 --- /dev/null +++ b/contracts/pair_stable/schema/migrate_msg.json @@ -0,0 +1,6 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "MigrateMsg", + "description": "This structure describes a migration message. We currently take no arguments for migrations.", + "type": "object" +} diff --git a/contracts/pair_stable/schema/pair_info.json b/contracts/pair_stable/schema/pair_info.json new file mode 100644 index 00000000..ce5d9c2b --- /dev/null +++ b/contracts/pair_stable/schema/pair_info.json @@ -0,0 +1,144 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "PairInfo", + "description": "This structure stores the main parameters for an Astroport pair", + "type": "object", + "required": [ + "asset_infos", + "contract_addr", + "liquidity_token", + "pair_type" + ], + "properties": { + "asset_infos": { + "description": "Asset information for the two assets in the pool", + "type": "array", + "items": { + "$ref": "#/definitions/AssetInfo" + }, + "maxItems": 2, + "minItems": 2 + }, + "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" + } + ] + } + }, + "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 astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"paloma...\") }; 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 + }, + { + "description": "Native token", + "type": "object", + "required": [ + "native_token" + ], + "properties": { + "native_token": { + "type": "object", + "required": [ + "denom" + ], + "properties": { + "denom": { + "type": "string" + } + } + } + }, + "additionalProperties": false + } + ] + }, + "PairType": { + "description": "This enum describes available pair types. ## Available pool types ``` # use astroport::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 + }, + { + "description": "Stable pair type", + "type": "object", + "required": [ + "stable" + ], + "properties": { + "stable": { + "type": "object" + } + }, + "additionalProperties": false + }, + { + "description": "Custom pair type", + "type": "object", + "required": [ + "custom" + ], + "properties": { + "custom": { + "type": "string" + } + }, + "additionalProperties": false + } + ] + } + } +} diff --git a/contracts/pair_stable/schema/pool_response.json b/contracts/pair_stable/schema/pool_response.json new file mode 100644 index 00000000..3a71ec92 --- /dev/null +++ b/contracts/pair_stable/schema/pool_response.json @@ -0,0 +1,113 @@ +{ + "$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 the two 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" + }, + "maxItems": 2, + "minItems": 2 + }, + "total_share": { + "description": "The total amount of LP tokens currently issued", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] + } + }, + "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": { + "title": "Description", + "description": "This enum describes a paloma 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" + } + ] + } + } + }, + "AssetInfo": { + "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"paloma...\") }; 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 + }, + { + "description": "Native token", + "type": "object", + "required": [ + "native_token" + ], + "properties": { + "native_token": { + "type": "object", + "required": [ + "denom" + ], + "properties": { + "denom": { + "type": "string" + } + } + } + }, + "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/contracts/pair_stable/schema/query_msg.json b/contracts/pair_stable/schema/query_msg.json new file mode 100644 index 00000000..412230e1 --- /dev/null +++ b/contracts/pair_stable/schema/query_msg.json @@ -0,0 +1,219 @@ +{ + "$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 + }, + { + "description": "Returns information about a pool in an object of type [`PoolResponse`].", + "type": "object", + "required": [ + "pool" + ], + "properties": { + "pool": { + "type": "object" + } + }, + "additionalProperties": false + }, + { + "description": "Returns contract configuration settings in a custom [`ConfigResponse`] structure.", + "type": "object", + "required": [ + "config" + ], + "properties": { + "config": { + "type": "object" + } + }, + "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 + }, + { + "description": "Returns information about a swap simulation in a [`SimulationResponse`] object.", + "type": "object", + "required": [ + "simulation" + ], + "properties": { + "simulation": { + "type": "object", + "required": [ + "offer_asset" + ], + "properties": { + "offer_asset": { + "$ref": "#/definitions/Asset" + } + } + } + }, + "additionalProperties": false + }, + { + "description": "Returns information about cumulative prices in a [`CumulativePricesResponse`] object.", + "type": "object", + "required": [ + "reverse_simulation" + ], + "properties": { + "reverse_simulation": { + "type": "object", + "required": [ + "ask_asset" + ], + "properties": { + "ask_asset": { + "$ref": "#/definitions/Asset" + } + } + } + }, + "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 + }, + { + "description": "Returns current D invariant in as a [`u128`] value", + "type": "object", + "required": [ + "query_compute_d" + ], + "properties": { + "query_compute_d": { + "type": "object" + } + }, + "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": { + "title": "Description", + "description": "This enum describes a paloma 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" + } + ] + } + } + }, + "AssetInfo": { + "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"paloma...\") }; 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 + }, + { + "description": "Native token", + "type": "object", + "required": [ + "native_token" + ], + "properties": { + "native_token": { + "type": "object", + "required": [ + "denom" + ], + "properties": { + "denom": { + "type": "string" + } + } + } + }, + "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/contracts/pair_stable/schema/reverse_simulation_response.json b/contracts/pair_stable/schema/reverse_simulation_response.json new file mode 100644 index 00000000..fafa524c --- /dev/null +++ b/contracts/pair_stable/schema/reverse_simulation_response.json @@ -0,0 +1,43 @@ +{ + "$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" + } + ] + } + }, + "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/contracts/pair_stable/schema/simulation_response.json b/contracts/pair_stable/schema/simulation_response.json new file mode 100644 index 00000000..1e23dd25 --- /dev/null +++ b/contracts/pair_stable/schema/simulation_response.json @@ -0,0 +1,43 @@ +{ + "$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" + } + ] + } + }, + "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/contracts/pair_stable/src/mock_querier.rs b/contracts/pair_stable/src/mock_querier.rs index 2882844b..50f4450e 100644 --- a/contracts/pair_stable/src/mock_querier.rs +++ b/contracts/pair_stable/src/mock_querier.rs @@ -133,7 +133,7 @@ impl WasmMockQuerier { } QueryRequest::Wasm(WasmQuery::Smart { contract_addr, msg }) => { if contract_addr == "factory" { - match from_binary(&msg).unwrap() { + match from_binary(msg).unwrap() { FeeInfo { .. } => SystemResult::Ok( to_binary(&FeeInfoResponse { fee_address: Some(Addr::unchecked("fee_address")), @@ -145,7 +145,7 @@ impl WasmMockQuerier { _ => panic!("DO NOT ENTER HERE"), } } else { - match from_binary(&msg).unwrap() { + match from_binary(msg).unwrap() { Cw20QueryMsg::TokenInfo {} => { let balances: &HashMap = match self.token_querier.balances.get(contract_addr) { @@ -166,7 +166,7 @@ impl WasmMockQuerier { name: "mAPPL".to_string(), symbol: "mAPPL".to_string(), decimals: 6, - total_supply: total_supply, + total_supply, }) .into(), ) diff --git a/contracts/pair_stable/src/testing.rs b/contracts/pair_stable/src/testing.rs index 568c2154..87a73a2a 100644 --- a/contracts/pair_stable/src/testing.rs +++ b/contracts/pair_stable/src/testing.rs @@ -41,7 +41,7 @@ fn store_liquidity_token(deps: DepsMut, msg_id: u64, contrac }), }; - let _res = reply(deps, mock_env(), reply_msg.clone()).unwrap(); + let _res = reply(deps, mock_env(), reply_msg).unwrap(); } #[test] @@ -69,9 +69,8 @@ fn proper_initialization() { 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(); + let res = instantiate(deps.as_mut(), mock_env(), info, msg).unwrap(); assert_eq!( res.messages, vec![SubMsg { @@ -153,7 +152,7 @@ fn provide_liquidity() { 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 _res = instantiate(deps.as_mut(), mock_env(), info, msg).unwrap(); // Store the liquidity token store_liquidity_token(deps.as_mut(), 1, "liquidity0000".to_string()); @@ -179,7 +178,6 @@ fn provide_liquidity() { receiver: None, }; - let env = mock_env(); let info = mock_info( "addr0000", &[Coin { @@ -187,7 +185,7 @@ fn provide_liquidity() { amount: Uint128::from(100_000000000000000000u128), }], ); - let res = execute(deps.as_mut(), env.clone().clone(), info, msg).unwrap(); + let res = execute(deps.as_mut(), mock_env(), 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!( @@ -283,7 +281,7 @@ fn provide_liquidity() { }], ); - let res: Response = execute(deps.as_mut(), env.clone(), info, msg).unwrap(); + let res: Response = execute(deps.as_mut(), env, 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!( @@ -345,7 +343,6 @@ fn provide_liquidity() { receiver: None, }; - let env = mock_env(); let info = mock_info( "addr0000", &[Coin { @@ -353,7 +350,7 @@ fn provide_liquidity() { amount: Uint128::from(100_000000000000000000u128), }], ); - let res = execute(deps.as_mut(), env.clone(), info, msg).unwrap_err(); + let res = execute(deps.as_mut(), mock_env(), info, msg).unwrap_err(); match res { ContractError::Std(StdError::GenericErr { msg, .. }) => assert_eq!( msg, @@ -427,7 +424,8 @@ fn provide_liquidity() { receiver: None, }; - let env = mock_env_with_block_time(env.block.time.seconds() + 1000); + let mut env_block_time_seconds = mock_env().block.time.seconds() + 1000; + let env = mock_env_with_block_time(env_block_time_seconds); let info = mock_info( "addr0001", &[Coin { @@ -435,7 +433,7 @@ fn provide_liquidity() { amount: Uint128::from(100_000000000000000000u128), }], ); - let _res = execute(deps.as_mut(), env.clone(), info, msg).unwrap(); + let _res = execute(deps.as_mut(), env, info, msg).unwrap(); // Initialize token balances with a ratio of 1:1 deps.querier.with_balance(&[( @@ -467,7 +465,8 @@ fn provide_liquidity() { receiver: None, }; - let env = mock_env_with_block_time(env.block.time.seconds() + 1000); + env_block_time_seconds += 1000; + let env = mock_env_with_block_time(env_block_time_seconds); let info = mock_info( "addr0001", &[Coin { @@ -514,10 +513,9 @@ fn withdraw_liquidity() { init_params: Some(to_binary(&StablePoolParams { amp: 100 }).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(); + let _res = instantiate(deps.as_mut(), mock_env(), info, msg).unwrap(); // Store the liquidity token store_liquidity_token(deps.as_mut(), 1, "liquidity0000".to_string()); @@ -529,9 +527,8 @@ fn withdraw_liquidity() { amount: Uint128::new(100u128), }); - let env = mock_env(); let info = mock_info("liquidity0000", &[]); - let res = execute(deps.as_mut(), env, info, msg).unwrap(); + let res = execute(deps.as_mut(), mock_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"); @@ -669,7 +666,7 @@ fn try_native_to_token() { }], ); - let res = execute(deps.as_mut(), env.clone(), info, msg).unwrap(); + let res = execute(deps.as_mut(), env, info, msg).unwrap(); let msg_transfer = res.messages.get(0).expect("no message"); let model: StableSwapModel = StableSwapModel::new( @@ -701,7 +698,7 @@ fn try_native_to_token() { let simulation_res: SimulationResponse = query_simulation( deps.as_ref(), - env.clone(), + mock_env(), Asset { info: AssetInfo::NativeToken { denom: "uusd".to_string(), @@ -737,7 +734,7 @@ fn try_native_to_token() { contract_addr: String::from("asset0000"), msg: to_binary(&Cw20ExecuteMsg::Transfer { recipient: String::from("addr0000"), - amount: Uint128::from(expected_return_amount), + amount: expected_return_amount, }) .unwrap(), funds: vec![], @@ -833,7 +830,7 @@ fn try_token_to_native() { 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 res = execute(deps.as_mut(), env, info, msg).unwrap(); let msg_transfer = res.messages.get(0).expect("no message"); let model: StableSwapModel = StableSwapModel::new( @@ -875,7 +872,7 @@ fn try_token_to_native() { let simulation_res: SimulationResponse = query_simulation( deps.as_ref(), - env.clone(), + mock_env(), Asset { amount: offer_amount, info: AssetInfo::Token { @@ -915,8 +912,7 @@ fn try_token_to_native() { .checked_sub(expected_tax_amount) .unwrap(), }], - }) - .into(), + }), id: 0, gas_limit: None, reply_on: ReplyOn::Never, @@ -991,7 +987,7 @@ fn test_deduct() { &[(&"uusd".to_string(), &Uint128::from(1000000u128))], ); - let amount = Uint128::new(1000_000_000u128); + let amount = Uint128::new(1_000_000_000_u128); let expected_after_amount = std::cmp::max( amount.checked_sub(amount * tax_rate).unwrap(), amount.checked_sub(tax_cap).unwrap(), @@ -1044,10 +1040,9 @@ fn test_query_pool() { init_params: Some(to_binary(&StablePoolParams { amp: 100 }).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(); + let _res = instantiate(deps.as_mut(), mock_env(), info, msg).unwrap(); // Store the liquidity token store_liquidity_token(deps.as_mut(), 1, "liquidity0000".to_string()); @@ -1109,10 +1104,9 @@ fn test_query_share() { init_params: Some(to_binary(&StablePoolParams { amp: 100 }).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(); + let _res = instantiate(deps.as_mut(), mock_env(), info, msg).unwrap(); // Store the liquidity token store_liquidity_token(deps.as_mut(), 1, "liquidity0000".to_string()); @@ -1165,7 +1159,7 @@ fn test_accumulate_prices() { Case { block_time: 1000, block_time_last: 1000, - last0: 1 * price_precision, + last0: price_precision, last1: 2 * price_precision, x_amount: 250_000000, y_amount: 500_000000, diff --git a/contracts/router/schema/config_response.json b/contracts/router/schema/config_response.json new file mode 100644 index 00000000..e63f1fe8 --- /dev/null +++ b/contracts/router/schema/config_response.json @@ -0,0 +1,15 @@ +{ + "$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": [ + "astroport_factory" + ], + "properties": { + "astroport_factory": { + "description": "The Astroport factory contract address", + "type": "string" + } + } +} diff --git a/contracts/router/schema/cw20_hook_msg.json b/contracts/router/schema/cw20_hook_msg.json new file mode 100644 index 00000000..aa1d517c --- /dev/null +++ b/contracts/router/schema/cw20_hook_msg.json @@ -0,0 +1,186 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Cw20HookMsg", + "oneOf": [ + { + "type": "object", + "required": [ + "execute_swap_operations" + ], + "properties": { + "execute_swap_operations": { + "type": "object", + "required": [ + "operations" + ], + "properties": { + "max_spread": { + "description": "Max spread", + "anyOf": [ + { + "$ref": "#/definitions/Decimal" + }, + { + "type": "null" + } + ] + }, + "minimum_receive": { + "description": "The minimum amount of tokens to get from a swap", + "anyOf": [ + { + "$ref": "#/definitions/Uint128" + }, + { + "type": "null" + } + ] + }, + "operations": { + "description": "A vector of swap operations", + "type": "array", + "items": { + "$ref": "#/definitions/SwapOperation" + } + }, + "to": { + "type": [ + "string", + "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" + }, + "AssetInfo": { + "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"paloma...\") }; 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 + }, + { + "description": "Native token", + "type": "object", + "required": [ + "native_token" + ], + "properties": { + "native_token": { + "type": "object", + "required": [ + "denom" + ], + "properties": { + "denom": { + "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 + }, + { + "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 + } + ] + }, + "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/contracts/router/schema/execute_msg.json b/contracts/router/schema/execute_msg.json new file mode 100644 index 00000000..5f52ab74 --- /dev/null +++ b/contracts/router/schema/execute_msg.json @@ -0,0 +1,296 @@ +{ + "$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": { + "anyOf": [ + { + "$ref": "#/definitions/Addr" + }, + { + "type": "null" + } + ] + } + } + } + }, + "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" + ], + "properties": { + "max_spread": { + "anyOf": [ + { + "$ref": "#/definitions/Decimal" + }, + { + "type": "null" + } + ] + }, + "operation": { + "$ref": "#/definitions/SwapOperation" + }, + "to": { + "type": [ + "string", + "null" + ] + } + } + } + }, + "additionalProperties": false + }, + { + "description": "Internal use AssertMinimumReceive checks that a receiver will get a minimum amount of tokens from a swap", + "type": "object", + "required": [ + "assert_minimum_receive" + ], + "properties": { + "assert_minimum_receive": { + "type": "object", + "required": [ + "asset_info", + "minimum_receive", + "prev_balance", + "receiver" + ], + "properties": { + "asset_info": { + "$ref": "#/definitions/AssetInfo" + }, + "minimum_receive": { + "$ref": "#/definitions/Uint128" + }, + "prev_balance": { + "$ref": "#/definitions/Uint128" + }, + "receiver": { + "type": "string" + } + } + } + }, + "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 astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"paloma...\") }; 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 + }, + { + "description": "Native token", + "type": "object", + "required": [ + "native_token" + ], + "properties": { + "native_token": { + "type": "object", + "required": [ + "denom" + ], + "properties": { + "denom": { + "type": "string" + } + } + } + }, + "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", + "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" + } + } + }, + "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 + }, + { + "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 + } + ] + }, + "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/contracts/router/schema/instantiate_msg.json b/contracts/router/schema/instantiate_msg.json new file mode 100644 index 00000000..06957f87 --- /dev/null +++ b/contracts/router/schema/instantiate_msg.json @@ -0,0 +1,15 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "InstantiateMsg", + "description": "This structure holds the parameters used for creating a contract.", + "type": "object", + "required": [ + "astroport_factory" + ], + "properties": { + "astroport_factory": { + "description": "The astroport factory contract address", + "type": "string" + } + } +} diff --git a/contracts/router/schema/migrate_msg.json b/contracts/router/schema/migrate_msg.json new file mode 100644 index 00000000..18c68d87 --- /dev/null +++ b/contracts/router/schema/migrate_msg.json @@ -0,0 +1,6 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "MigrateMsg", + "description": "This structure describes a migration message. We currently take no arguments for migrations.", + "type": "object" +} diff --git a/contracts/router/schema/query_msg.json b/contracts/router/schema/query_msg.json new file mode 100644 index 00000000..f1a1afa0 --- /dev/null +++ b/contracts/router/schema/query_msg.json @@ -0,0 +1,178 @@ +{ + "$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 + }, + { + "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 + } + ], + "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 astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"paloma...\") }; 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 + }, + { + "description": "Native token", + "type": "object", + "required": [ + "native_token" + ], + "properties": { + "native_token": { + "type": "object", + "required": [ + "denom" + ], + "properties": { + "denom": { + "type": "string" + } + } + } + }, + "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 + }, + { + "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 + } + ] + }, + "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/contracts/router/schema/simulate_swap_operations_response.json b/contracts/router/schema/simulate_swap_operations_response.json new file mode 100644 index 00000000..360615be --- /dev/null +++ b/contracts/router/schema/simulate_swap_operations_response.json @@ -0,0 +1,25 @@ +{ + "$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" + } + ] + } + }, + "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/contracts/router/src/contract.rs b/contracts/router/src/contract.rs index 84587373..15e480b2 100644 --- a/contracts/router/src/contract.rs +++ b/contracts/router/src/contract.rs @@ -518,114 +518,105 @@ 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, &vec![]).is_err()); + assert!(assert_operations(deps.as_ref().api, &[]).is_err()); // uluna output - assert_eq!( - true, - assert_operations( - deps.as_ref().api, - &vec![ - SwapOperation::NativeSwap { - offer_denom: "uusd".to_string(), - ask_denom: "uluna".to_string(), + assert!(assert_operations( + deps.as_ref().api, + &vec![ + SwapOperation::NativeSwap { + offer_denom: "uusd".to_string(), + ask_denom: "uluna".to_string(), + }, + SwapOperation::AstroSwap { + offer_asset_info: AssetInfo::NativeToken { + denom: "ukrw".to_string(), }, - SwapOperation::AstroSwap { - offer_asset_info: AssetInfo::NativeToken { - denom: "ukrw".to_string(), - }, - ask_asset_info: AssetInfo::Token { - contract_addr: Addr::unchecked("asset0001"), - }, + 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() - ); + }, + 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::NativeSwap { - offer_denom: "uusd".to_string(), - ask_denom: "uluna".to_string(), + assert!(assert_operations( + deps.as_ref().api, + &vec![ + SwapOperation::NativeSwap { + offer_denom: "uusd".to_string(), + ask_denom: "uluna".to_string(), + }, + SwapOperation::AstroSwap { + offer_asset_info: AssetInfo::NativeToken { + denom: "ukrw".to_string(), }, - SwapOperation::AstroSwap { - offer_asset_info: AssetInfo::NativeToken { - denom: "ukrw".to_string(), - }, - ask_asset_info: AssetInfo::Token { - contract_addr: Addr::unchecked("asset0001"), - }, + 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::Token { + contract_addr: Addr::unchecked("asset0001"), }, - SwapOperation::AstroSwap { - offer_asset_info: AssetInfo::NativeToken { - denom: "uluna".to_string(), - }, - ask_asset_info: AssetInfo::Token { - contract_addr: Addr::unchecked("asset0002"), - }, + 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() - ); + }, + ] + ) + .is_ok()); // Multiple output token type errors - assert_eq!( - true, - assert_operations( - deps.as_ref().api, - &vec![ - SwapOperation::NativeSwap { - offer_denom: "uusd".to_string(), - ask_denom: "ukrw".to_string(), + assert!(assert_operations( + deps.as_ref().api, + &vec![ + SwapOperation::NativeSwap { + offer_denom: "uusd".to_string(), + ask_denom: "ukrw".to_string(), + }, + SwapOperation::AstroSwap { + offer_asset_info: AssetInfo::NativeToken { + denom: "ukrw".to_string(), }, - SwapOperation::AstroSwap { - offer_asset_info: AssetInfo::NativeToken { - denom: "ukrw".to_string(), - }, - ask_asset_info: AssetInfo::Token { - contract_addr: Addr::unchecked("asset0001"), - }, + 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::Token { + contract_addr: Addr::unchecked("asset0001"), }, - SwapOperation::AstroSwap { - offer_asset_info: AssetInfo::NativeToken { - denom: "uluna".to_string(), - }, - ask_asset_info: AssetInfo::Token { - contract_addr: Addr::unchecked("asset0002"), - }, + 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() - ); + }, + ] + ) + .is_err()); } diff --git a/contracts/router/src/testing/mock_querier.rs b/contracts/router/src/testing/mock_querier.rs index a244afd4..3ecf3f9e 100644 --- a/contracts/router/src/testing/mock_querier.rs +++ b/contracts/router/src/testing/mock_querier.rs @@ -196,7 +196,7 @@ impl WasmMockQuerier { } fn handle_default(&self, msg: &Binary) -> QuerierResult { - match from_binary(&msg).unwrap() { + match from_binary(msg).unwrap() { QueryMsg::Pair { asset_infos } => { let key = asset_infos[0].to_string() + asset_infos[1].to_string().as_str(); match self.astroport_factory_querier.pairs.get(&key) { @@ -230,7 +230,7 @@ impl WasmMockQuerier { } fn handle_cw20(&self, contract_addr: &String, msg: &Binary) -> QuerierResult { - match from_binary(&msg).unwrap() { + match from_binary(msg).unwrap() { Cw20QueryMsg::TokenInfo {} => { let balances: &HashMap = match self.token_querier.balances.get(contract_addr) { @@ -250,7 +250,7 @@ impl WasmMockQuerier { name: "mAPPL".to_string(), symbol: "mAPPL".to_string(), decimals: 6, - total_supply: total_supply, + total_supply, }))) } Cw20QueryMsg::Balance { address } => { @@ -289,8 +289,8 @@ impl WasmMockQuerier { } pub fn with_balance(&mut self, balances: &[(&String, &[Coin])]) { - for (addr, balance) in balances { - self.base.update_balance(addr.clone(), balance.to_vec()); + for &(addr, balance) in balances { + self.base.update_balance(addr, balance.to_vec()); } } diff --git a/contracts/router/src/testing/tests.rs b/contracts/router/src/testing/tests.rs index 9a06830c..00b5c6bb 100644 --- a/contracts/router/src/testing/tests.rs +++ b/contracts/router/src/testing/tests.rs @@ -421,7 +421,7 @@ fn execute_swap_operation() { }; let env = mock_env(); let info = mock_info(MOCK_CONTRACT_ADDR, &[]); - let res = execute(deps.as_mut(), env, info, msg.clone()).unwrap(); + let res = execute(deps.as_mut(), env, info, msg).unwrap(); assert_eq!( res.messages, vec![SubMsg { @@ -614,7 +614,7 @@ fn assert_minimum_receive_native_token() { minimum_receive: Uint128::from(1000001u128), receiver: String::from("addr0000"), }; - let res = execute(deps.as_mut(), env.clone(), info, msg).unwrap_err(); + let res = execute(deps.as_mut(), env, info, msg).unwrap_err(); assert_eq!( res, ContractError::AssertionMinimumReceive { @@ -655,7 +655,7 @@ fn assert_minimum_receive_token() { minimum_receive: Uint128::from(1000001u128), receiver: String::from("addr0000"), }; - let res = execute(deps.as_mut(), env.clone(), info, msg).unwrap_err(); + let res = execute(deps.as_mut(), env, info, msg).unwrap_err(); assert_eq!( res, ContractError::AssertionMinimumReceive { diff --git a/contracts/token/schema/all_accounts_response.json b/contracts/token/schema/all_accounts_response.json new file mode 100644 index 00000000..cea50fba --- /dev/null +++ b/contracts/token/schema/all_accounts_response.json @@ -0,0 +1,16 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "AllAccountsResponse", + "type": "object", + "required": [ + "accounts" + ], + "properties": { + "accounts": { + "type": "array", + "items": { + "type": "string" + } + } + } +} diff --git a/contracts/token/schema/all_allowances_response.json b/contracts/token/schema/all_allowances_response.json new file mode 100644 index 00000000..6bb2291c --- /dev/null +++ b/contracts/token/schema/all_allowances_response.json @@ -0,0 +1,99 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "AllAllowancesResponse", + "type": "object", + "required": [ + "allowances" + ], + "properties": { + "allowances": { + "type": "array", + "items": { + "$ref": "#/definitions/AllowanceInfo" + } + } + }, + "definitions": { + "AllowanceInfo": { + "type": "object", + "required": [ + "allowance", + "expires", + "spender" + ], + "properties": { + "allowance": { + "$ref": "#/definitions/Uint128" + }, + "expires": { + "$ref": "#/definitions/Expiration" + }, + "spender": { + "type": "string" + } + } + }, + "Expiration": { + "description": "Expiration represents a point in time when some event happens. It can compare with a BlockInfo and will return is_expired() == true once the condition is hit (and for every block in the future)", + "oneOf": [ + { + "description": "AtHeight will expire when `env.block.height` >= height", + "type": "object", + "required": [ + "at_height" + ], + "properties": { + "at_height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + { + "description": "AtTime will expire when `env.block.time` >= time", + "type": "object", + "required": [ + "at_time" + ], + "properties": { + "at_time": { + "$ref": "#/definitions/Timestamp" + } + }, + "additionalProperties": false + }, + { + "description": "Never will never expire. Used to express the empty variant", + "type": "object", + "required": [ + "never" + ], + "properties": { + "never": { + "type": "object" + } + }, + "additionalProperties": false + } + ] + }, + "Timestamp": { + "description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```", + "allOf": [ + { + "$ref": "#/definitions/Uint64" + } + ] + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + }, + "Uint64": { + "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```", + "type": "string" + } + } +} diff --git a/contracts/token/schema/allowance_response.json b/contracts/token/schema/allowance_response.json new file mode 100644 index 00000000..c4f98d6f --- /dev/null +++ b/contracts/token/schema/allowance_response.json @@ -0,0 +1,81 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "AllowanceResponse", + "type": "object", + "required": [ + "allowance", + "expires" + ], + "properties": { + "allowance": { + "$ref": "#/definitions/Uint128" + }, + "expires": { + "$ref": "#/definitions/Expiration" + } + }, + "definitions": { + "Expiration": { + "description": "Expiration represents a point in time when some event happens. It can compare with a BlockInfo and will return is_expired() == true once the condition is hit (and for every block in the future)", + "oneOf": [ + { + "description": "AtHeight will expire when `env.block.height` >= height", + "type": "object", + "required": [ + "at_height" + ], + "properties": { + "at_height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + { + "description": "AtTime will expire when `env.block.time` >= time", + "type": "object", + "required": [ + "at_time" + ], + "properties": { + "at_time": { + "$ref": "#/definitions/Timestamp" + } + }, + "additionalProperties": false + }, + { + "description": "Never will never expire. Used to express the empty variant", + "type": "object", + "required": [ + "never" + ], + "properties": { + "never": { + "type": "object" + } + }, + "additionalProperties": false + } + ] + }, + "Timestamp": { + "description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```", + "allOf": [ + { + "$ref": "#/definitions/Uint64" + } + ] + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + }, + "Uint64": { + "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```", + "type": "string" + } + } +} diff --git a/contracts/token/schema/balance_response.json b/contracts/token/schema/balance_response.json new file mode 100644 index 00000000..4e1a0be2 --- /dev/null +++ b/contracts/token/schema/balance_response.json @@ -0,0 +1,19 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "BalanceResponse", + "type": "object", + "required": [ + "balance" + ], + "properties": { + "balance": { + "$ref": "#/definitions/Uint128" + } + }, + "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/contracts/token/schema/execute_msg.json b/contracts/token/schema/execute_msg.json new file mode 100644 index 00000000..eda0f5dc --- /dev/null +++ b/contracts/token/schema/execute_msg.json @@ -0,0 +1,442 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ExecuteMsg", + "oneOf": [ + { + "description": "Transfer is a base message to move tokens to another account without triggering actions", + "type": "object", + "required": [ + "transfer" + ], + "properties": { + "transfer": { + "type": "object", + "required": [ + "amount", + "recipient" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "recipient": { + "type": "string" + } + } + } + }, + "additionalProperties": false + }, + { + "description": "Burn is a base message to destroy tokens forever", + "type": "object", + "required": [ + "burn" + ], + "properties": { + "burn": { + "type": "object", + "required": [ + "amount" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + } + } + } + }, + "additionalProperties": false + }, + { + "description": "Send is a base message to transfer tokens to a contract and trigger an action on the receiving contract.", + "type": "object", + "required": [ + "send" + ], + "properties": { + "send": { + "type": "object", + "required": [ + "amount", + "contract", + "msg" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "contract": { + "type": "string" + }, + "msg": { + "$ref": "#/definitions/Binary" + } + } + } + }, + "additionalProperties": false + }, + { + "description": "Only with \"approval\" extension. Allows spender to access an additional amount tokens from the owner's (env.sender) account. If expires is Some(), overwrites current allowance expiration with this one.", + "type": "object", + "required": [ + "increase_allowance" + ], + "properties": { + "increase_allowance": { + "type": "object", + "required": [ + "amount", + "spender" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "expires": { + "anyOf": [ + { + "$ref": "#/definitions/Expiration" + }, + { + "type": "null" + } + ] + }, + "spender": { + "type": "string" + } + } + } + }, + "additionalProperties": false + }, + { + "description": "Only with \"approval\" extension. Lowers the spender's access of tokens from the owner's (env.sender) account by amount. If expires is Some(), overwrites current allowance expiration with this one.", + "type": "object", + "required": [ + "decrease_allowance" + ], + "properties": { + "decrease_allowance": { + "type": "object", + "required": [ + "amount", + "spender" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "expires": { + "anyOf": [ + { + "$ref": "#/definitions/Expiration" + }, + { + "type": "null" + } + ] + }, + "spender": { + "type": "string" + } + } + } + }, + "additionalProperties": false + }, + { + "description": "Only with \"approval\" extension. Transfers amount tokens from owner -> recipient if `env.sender` has sufficient pre-approval.", + "type": "object", + "required": [ + "transfer_from" + ], + "properties": { + "transfer_from": { + "type": "object", + "required": [ + "amount", + "owner", + "recipient" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "owner": { + "type": "string" + }, + "recipient": { + "type": "string" + } + } + } + }, + "additionalProperties": false + }, + { + "description": "Only with \"approval\" extension. Sends amount tokens from owner -> contract if `env.sender` has sufficient pre-approval.", + "type": "object", + "required": [ + "send_from" + ], + "properties": { + "send_from": { + "type": "object", + "required": [ + "amount", + "contract", + "msg", + "owner" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "contract": { + "type": "string" + }, + "msg": { + "$ref": "#/definitions/Binary" + }, + "owner": { + "type": "string" + } + } + } + }, + "additionalProperties": false + }, + { + "description": "Only with \"approval\" extension. Destroys tokens forever", + "type": "object", + "required": [ + "burn_from" + ], + "properties": { + "burn_from": { + "type": "object", + "required": [ + "amount", + "owner" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "owner": { + "type": "string" + } + } + } + }, + "additionalProperties": false + }, + { + "description": "Only with the \"mintable\" extension. If authorized, creates amount new tokens and adds to the recipient balance.", + "type": "object", + "required": [ + "mint" + ], + "properties": { + "mint": { + "type": "object", + "required": [ + "amount", + "recipient" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "recipient": { + "type": "string" + } + } + } + }, + "additionalProperties": false + }, + { + "description": "Only with the \"marketing\" extension. If authorized, updates marketing metadata. Setting None/null for any of these will leave it unchanged. Setting Some(\"\") will clear this field on the contract storage", + "type": "object", + "required": [ + "update_marketing" + ], + "properties": { + "update_marketing": { + "type": "object", + "properties": { + "description": { + "description": "A longer description of the token and it's utility. Designed for tooltips or such", + "type": [ + "string", + "null" + ] + }, + "marketing": { + "description": "The address (if any) who can update this data structure", + "type": [ + "string", + "null" + ] + }, + "project": { + "description": "A URL pointing to the project behind this token.", + "type": [ + "string", + "null" + ] + } + } + } + }, + "additionalProperties": false + }, + { + "description": "If set as the \"marketing\" role on the contract, upload a new URL, SVG, or PNG for the token", + "type": "object", + "required": [ + "upload_logo" + ], + "properties": { + "upload_logo": { + "$ref": "#/definitions/Logo" + } + }, + "additionalProperties": false + } + ], + "definitions": { + "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", + "type": "string" + }, + "EmbeddedLogo": { + "description": "This is used to store the logo on the blockchain in an accepted format. Enforce maximum size of 5KB on all variants.", + "oneOf": [ + { + "description": "Store the Logo as an SVG file. The content must conform to the spec at https://en.wikipedia.org/wiki/Scalable_Vector_Graphics (The contract should do some light-weight sanity-check validation)", + "type": "object", + "required": [ + "svg" + ], + "properties": { + "svg": { + "$ref": "#/definitions/Binary" + } + }, + "additionalProperties": false + }, + { + "description": "Store the Logo as a PNG file. This will likely only support up to 64x64 or so within the 5KB limit.", + "type": "object", + "required": [ + "png" + ], + "properties": { + "png": { + "$ref": "#/definitions/Binary" + } + }, + "additionalProperties": false + } + ] + }, + "Expiration": { + "description": "Expiration represents a point in time when some event happens. It can compare with a BlockInfo and will return is_expired() == true once the condition is hit (and for every block in the future)", + "oneOf": [ + { + "description": "AtHeight will expire when `env.block.height` >= height", + "type": "object", + "required": [ + "at_height" + ], + "properties": { + "at_height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + { + "description": "AtTime will expire when `env.block.time` >= time", + "type": "object", + "required": [ + "at_time" + ], + "properties": { + "at_time": { + "$ref": "#/definitions/Timestamp" + } + }, + "additionalProperties": false + }, + { + "description": "Never will never expire. Used to express the empty variant", + "type": "object", + "required": [ + "never" + ], + "properties": { + "never": { + "type": "object" + } + }, + "additionalProperties": false + } + ] + }, + "Logo": { + "description": "This is used for uploading logo data, or setting it in InstantiateData", + "oneOf": [ + { + "description": "A reference to an externally hosted logo. Must be a valid HTTP or HTTPS URL.", + "type": "object", + "required": [ + "url" + ], + "properties": { + "url": { + "type": "string" + } + }, + "additionalProperties": false + }, + { + "description": "Logo content stored on the blockchain. Enforce maximum size of 5KB on all variants", + "type": "object", + "required": [ + "embedded" + ], + "properties": { + "embedded": { + "$ref": "#/definitions/EmbeddedLogo" + } + }, + "additionalProperties": false + } + ] + }, + "Timestamp": { + "description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```", + "allOf": [ + { + "$ref": "#/definitions/Uint64" + } + ] + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + }, + "Uint64": { + "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```", + "type": "string" + } + } +} diff --git a/contracts/token/schema/instantiate_msg.json b/contracts/token/schema/instantiate_msg.json new file mode 100644 index 00000000..a31b33a3 --- /dev/null +++ b/contracts/token/schema/instantiate_msg.json @@ -0,0 +1,89 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "InstantiateMsg", + "description": "This structure describes the parameters used for creating a token contract. TokenContract InstantiateMsg", + "type": "object", + "required": [ + "decimals", + "initial_balances", + "name", + "symbol" + ], + "properties": { + "decimals": { + "description": "The amount of decimals the token has", + "type": "integer", + "format": "uint8", + "minimum": 0.0 + }, + "initial_balances": { + "description": "Initial token balances", + "type": "array", + "items": { + "$ref": "#/definitions/Cw20Coin" + } + }, + "mint": { + "description": "Minting controls specified in a [`MinterResponse`] structure", + "anyOf": [ + { + "$ref": "#/definitions/MinterResponse" + }, + { + "type": "null" + } + ] + }, + "name": { + "description": "Token name", + "type": "string" + }, + "symbol": { + "description": "Token symbol", + "type": "string" + } + }, + "definitions": { + "Cw20Coin": { + "type": "object", + "required": [ + "address", + "amount" + ], + "properties": { + "address": { + "type": "string" + }, + "amount": { + "$ref": "#/definitions/Uint128" + } + } + }, + "MinterResponse": { + "type": "object", + "required": [ + "minter" + ], + "properties": { + "cap": { + "description": "cap is a hard cap on total supply that can be achieved by minting. Note that this refers to total_supply. If None, there is unlimited cap.", + "anyOf": [ + { + "$ref": "#/definitions/Uint128" + }, + { + "type": "null" + } + ] + }, + "minter": { + "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/contracts/token/schema/minter_response.json b/contracts/token/schema/minter_response.json new file mode 100644 index 00000000..a05f8fd5 --- /dev/null +++ b/contracts/token/schema/minter_response.json @@ -0,0 +1,30 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "MinterResponse", + "type": "object", + "required": [ + "minter" + ], + "properties": { + "cap": { + "description": "cap is a hard cap on total supply that can be achieved by minting. Note that this refers to total_supply. If None, there is unlimited cap.", + "anyOf": [ + { + "$ref": "#/definitions/Uint128" + }, + { + "type": "null" + } + ] + }, + "minter": { + "type": "string" + } + }, + "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/contracts/token/schema/query_msg.json b/contracts/token/schema/query_msg.json new file mode 100644 index 00000000..7c33c352 --- /dev/null +++ b/contracts/token/schema/query_msg.json @@ -0,0 +1,168 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "QueryMsg", + "oneOf": [ + { + "description": "Returns the current balance of the given address, 0 if unset. Return type: BalanceResponse.", + "type": "object", + "required": [ + "balance" + ], + "properties": { + "balance": { + "type": "object", + "required": [ + "address" + ], + "properties": { + "address": { + "type": "string" + } + } + } + }, + "additionalProperties": false + }, + { + "description": "Returns metadata on the contract - name, decimals, supply, etc. Return type: TokenInfoResponse.", + "type": "object", + "required": [ + "token_info" + ], + "properties": { + "token_info": { + "type": "object" + } + }, + "additionalProperties": false + }, + { + "description": "Only with \"mintable\" extension. Returns who can mint and the hard cap on maximum tokens after minting. Return type: MinterResponse.", + "type": "object", + "required": [ + "minter" + ], + "properties": { + "minter": { + "type": "object" + } + }, + "additionalProperties": false + }, + { + "description": "Only with \"allowance\" extension. Returns how much spender can use from owner account, 0 if unset. Return type: AllowanceResponse.", + "type": "object", + "required": [ + "allowance" + ], + "properties": { + "allowance": { + "type": "object", + "required": [ + "owner", + "spender" + ], + "properties": { + "owner": { + "type": "string" + }, + "spender": { + "type": "string" + } + } + } + }, + "additionalProperties": false + }, + { + "description": "Only with \"enumerable\" extension (and \"allowances\") Returns all allowances this owner has approved. Supports pagination. Return type: AllAllowancesResponse.", + "type": "object", + "required": [ + "all_allowances" + ], + "properties": { + "all_allowances": { + "type": "object", + "required": [ + "owner" + ], + "properties": { + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "owner": { + "type": "string" + }, + "start_after": { + "type": [ + "string", + "null" + ] + } + } + } + }, + "additionalProperties": false + }, + { + "description": "Only with \"enumerable\" extension Returns all accounts that have balances. Supports pagination. Return type: AllAccountsResponse.", + "type": "object", + "required": [ + "all_accounts" + ], + "properties": { + "all_accounts": { + "type": "object", + "properties": { + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "type": [ + "string", + "null" + ] + } + } + } + }, + "additionalProperties": false + }, + { + "description": "Only with \"marketing\" extension Returns more metadata on the contract to display in the client: - description, logo, project url, etc. Return type: MarketingInfoResponse", + "type": "object", + "required": [ + "marketing_info" + ], + "properties": { + "marketing_info": { + "type": "object" + } + }, + "additionalProperties": false + }, + { + "description": "Only with \"marketing\" extension Downloads the embedded logo data (if stored on chain). Errors if no logo data is stored for this contract. Return type: DownloadLogoResponse.", + "type": "object", + "required": [ + "download_logo" + ], + "properties": { + "download_logo": { + "type": "object" + } + }, + "additionalProperties": false + } + ] +} diff --git a/contracts/token/schema/token_info_response.json b/contracts/token/schema/token_info_response.json new file mode 100644 index 00000000..9920c841 --- /dev/null +++ b/contracts/token/schema/token_info_response.json @@ -0,0 +1,33 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "TokenInfoResponse", + "type": "object", + "required": [ + "decimals", + "name", + "symbol", + "total_supply" + ], + "properties": { + "decimals": { + "type": "integer", + "format": "uint8", + "minimum": 0.0 + }, + "name": { + "type": "string" + }, + "symbol": { + "type": "string" + }, + "total_supply": { + "$ref": "#/definitions/Uint128" + } + }, + "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/contracts/tokenomics/maker/schema/config_response.json b/contracts/tokenomics/maker/schema/config_response.json new file mode 100644 index 00000000..55730661 --- /dev/null +++ b/contracts/tokenomics/maker/schema/config_response.json @@ -0,0 +1,111 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ConfigResponse", + "description": "A custom struct that holds contract parameters and is used to retrieve them.", + "type": "object", + "required": [ + "astro_token_contract", + "factory_contract", + "governance_percent", + "max_spread", + "owner", + "pre_upgrade_astro_amount", + "remainder_reward", + "staking_contract" + ], + "properties": { + "astro_token_contract": { + "description": "The ASTRO token contract address", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + }, + "factory_contract": { + "description": "The factory contract address", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + }, + "governance_contract": { + "description": "The governance contract address (fee distributor for vxASTRO stakers)", + "anyOf": [ + { + "$ref": "#/definitions/Addr" + }, + { + "type": "null" + } + ] + }, + "governance_percent": { + "description": "The percentage of fees that go to governance_contract", + "allOf": [ + { + "$ref": "#/definitions/Uint64" + } + ] + }, + "max_spread": { + "description": "The maximum spread used when swapping fee tokens to ASTRO", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "owner": { + "description": "Address that is allowed to update contract parameters", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + }, + "pre_upgrade_astro_amount": { + "description": "The amount of ASTRO tokens accrued before upgrading the Maker implementation and enabling reward distribution", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] + }, + "remainder_reward": { + "description": "The remainder ASTRO tokens (accrued before the Maker is upgraded) to be distributed to xASTRO stakers", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] + }, + "staking_contract": { + "description": "The xASTRO staking contract address", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + } + }, + "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" + }, + "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" + }, + "Uint64": { + "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```", + "type": "string" + } + } +} diff --git a/contracts/tokenomics/maker/schema/execute_msg.json b/contracts/tokenomics/maker/schema/execute_msg.json new file mode 100644 index 00000000..1be0b751 --- /dev/null +++ b/contracts/tokenomics/maker/schema/execute_msg.json @@ -0,0 +1,382 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ExecuteMsg", + "description": "This structure describes the functions that can be executed in this contract.", + "oneOf": [ + { + "description": "Collects and swaps fee tokens to ASTRO", + "type": "object", + "required": [ + "collect" + ], + "properties": { + "collect": { + "type": "object", + "required": [ + "assets" + ], + "properties": { + "assets": { + "description": "The assets to swap to ASTRO", + "type": "array", + "items": { + "$ref": "#/definitions/AssetWithLimit" + } + } + } + } + }, + "additionalProperties": false + }, + { + "description": "Updates general settings", + "type": "object", + "required": [ + "update_config" + ], + "properties": { + "update_config": { + "type": "object", + "properties": { + "factory_contract": { + "description": "The factory contract address", + "type": [ + "string", + "null" + ] + }, + "governance_contract": { + "description": "The governance contract address (fee distributor for vxASTRO)", + "anyOf": [ + { + "$ref": "#/definitions/UpdateAddr" + }, + { + "type": "null" + } + ] + }, + "governance_percent": { + "description": "The percentage of fees that go to governance_contract", + "anyOf": [ + { + "$ref": "#/definitions/Uint64" + }, + { + "type": "null" + } + ] + }, + "max_spread": { + "description": "The maximum spread used when swapping fee tokens to ASTRO", + "anyOf": [ + { + "$ref": "#/definitions/Decimal" + }, + { + "type": "null" + } + ] + }, + "staking_contract": { + "description": "The xASTRO staking contract address", + "type": [ + "string", + "null" + ] + } + } + } + }, + "additionalProperties": false + }, + { + "description": "Add bridge tokens used to swap specific fee tokens to ASTRO (effectively declaring a swap route)", + "type": "object", + "required": [ + "update_bridges" + ], + "properties": { + "update_bridges": { + "type": "object", + "properties": { + "add": { + "type": [ + "array", + "null" + ], + "items": { + "type": "array", + "items": [ + { + "$ref": "#/definitions/AssetInfo" + }, + { + "$ref": "#/definitions/AssetInfo" + } + ], + "maxItems": 2, + "minItems": 2 + } + }, + "remove": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/AssetInfo" + } + } + } + } + }, + "additionalProperties": false + }, + { + "description": "Swap fee tokens via bridge assets", + "type": "object", + "required": [ + "swap_bridge_assets" + ], + "properties": { + "swap_bridge_assets": { + "type": "object", + "required": [ + "assets", + "depth" + ], + "properties": { + "assets": { + "type": "array", + "items": { + "$ref": "#/definitions/AssetInfo" + } + }, + "depth": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + } + } + }, + "additionalProperties": false + }, + { + "description": "Distribute ASTRO to stakers and to governance", + "type": "object", + "required": [ + "distribute_astro" + ], + "properties": { + "distribute_astro": { + "type": "object" + } + }, + "additionalProperties": false + }, + { + "description": "Creates a request to change the contract's ownership", + "type": "object", + "required": [ + "propose_new_owner" + ], + "properties": { + "propose_new_owner": { + "type": "object", + "required": [ + "expires_in", + "owner" + ], + "properties": { + "expires_in": { + "description": "The validity period of the proposal to change the owner", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "owner": { + "description": "The newly proposed owner", + "type": "string" + } + } + } + }, + "additionalProperties": false + }, + { + "description": "Removes a request to change contract ownership", + "type": "object", + "required": [ + "drop_ownership_proposal" + ], + "properties": { + "drop_ownership_proposal": { + "type": "object" + } + }, + "additionalProperties": false + }, + { + "description": "Claims contract ownership", + "type": "object", + "required": [ + "claim_ownership" + ], + "properties": { + "claim_ownership": { + "type": "object" + } + }, + "additionalProperties": false + }, + { + "description": "Enables the distribution of current fees accrued in the contract over \"blocks\" number of blocks", + "type": "object", + "required": [ + "enable_rewards" + ], + "properties": { + "enable_rewards": { + "type": "object", + "required": [ + "blocks" + ], + "properties": { + "blocks": { + "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 astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"paloma...\") }; 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 + }, + { + "description": "Native token", + "type": "object", + "required": [ + "native_token" + ], + "properties": { + "native_token": { + "type": "object", + "required": [ + "denom" + ], + "properties": { + "denom": { + "type": "string" + } + } + } + }, + "additionalProperties": false + } + ] + }, + "AssetWithLimit": { + "description": "This struct holds parameters to help with swapping a specific amount of a fee token to ASTRO.", + "type": "object", + "required": [ + "info" + ], + "properties": { + "info": { + "description": "Information about the fee token to swap", + "allOf": [ + { + "$ref": "#/definitions/AssetInfo" + } + ] + }, + "limit": { + "description": "The amount of tokens to swap", + "anyOf": [ + { + "$ref": "#/definitions/Uint128" + }, + { + "type": "null" + } + ] + } + } + }, + "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" + }, + "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" + }, + "UpdateAddr": { + "description": "This is an enum used for setting and removing a contract address.", + "oneOf": [ + { + "description": "Sets a new contract address.", + "type": "object", + "required": [ + "set" + ], + "properties": { + "set": { + "type": "string" + } + }, + "additionalProperties": false + }, + { + "description": "Removes a contract address.", + "type": "object", + "required": [ + "remove" + ], + "properties": { + "remove": { + "type": "object" + } + }, + "additionalProperties": false + } + ] + } + } +} diff --git a/contracts/tokenomics/maker/schema/instantiate_msg.json b/contracts/tokenomics/maker/schema/instantiate_msg.json new file mode 100644 index 00000000..011713b7 --- /dev/null +++ b/contracts/tokenomics/maker/schema/instantiate_msg.json @@ -0,0 +1,69 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "InstantiateMsg", + "description": "This structure stores general parameters for the contract.", + "type": "object", + "required": [ + "astro_token_contract", + "factory_contract", + "owner", + "staking_contract" + ], + "properties": { + "astro_token_contract": { + "description": "The ASTRO token contract address", + "type": "string" + }, + "factory_contract": { + "description": "The factory contract address", + "type": "string" + }, + "governance_contract": { + "description": "The governance contract address (fee distributor for vxASTRO)", + "type": [ + "string", + "null" + ] + }, + "governance_percent": { + "description": "The percentage of fees that go to governance_contract", + "anyOf": [ + { + "$ref": "#/definitions/Uint64" + }, + { + "type": "null" + } + ] + }, + "max_spread": { + "description": "The maximum spread used when swapping fee tokens to ASTRO", + "anyOf": [ + { + "$ref": "#/definitions/Decimal" + }, + { + "type": "null" + } + ] + }, + "owner": { + "description": "Address that's allowed to change contract parameters", + "type": "string" + }, + "staking_contract": { + "description": "The xASTRO staking contract address", + "type": "string" + } + }, + "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" + }, + "Uint64": { + "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```", + "type": "string" + } + } +} diff --git a/contracts/tokenomics/maker/schema/query_msg.json b/contracts/tokenomics/maker/schema/query_msg.json new file mode 100644 index 00000000..7384d2e4 --- /dev/null +++ b/contracts/tokenomics/maker/schema/query_msg.json @@ -0,0 +1,109 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "QueryMsg", + "description": "This structure describes the query functions available in the contract.", + "oneOf": [ + { + "description": "Returns information about the maker configs that contains in the [`ConfigResponse`]", + "type": "object", + "required": [ + "config" + ], + "properties": { + "config": { + "type": "object" + } + }, + "additionalProperties": false + }, + { + "description": "Returns the balance for each asset in the specified input parameters", + "type": "object", + "required": [ + "balances" + ], + "properties": { + "balances": { + "type": "object", + "required": [ + "assets" + ], + "properties": { + "assets": { + "type": "array", + "items": { + "$ref": "#/definitions/AssetInfo" + } + } + } + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "bridges" + ], + "properties": { + "bridges": { + "type": "object" + } + }, + "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 astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"paloma...\") }; 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 + }, + { + "description": "Native token", + "type": "object", + "required": [ + "native_token" + ], + "properties": { + "native_token": { + "type": "object", + "required": [ + "denom" + ], + "properties": { + "denom": { + "type": "string" + } + } + } + }, + "additionalProperties": false + } + ] + } + } +} diff --git a/contracts/tokenomics/maker/src/testing.rs b/contracts/tokenomics/maker/src/testing.rs index 10e31bc8..791f89d7 100644 --- a/contracts/tokenomics/maker/src/testing.rs +++ b/contracts/tokenomics/maker/src/testing.rs @@ -146,6 +146,6 @@ fn update_owner() { // Let's query the state let config: ConfigResponse = - from_binary(&query(deps.as_ref(), env.clone(), QueryMsg::Config {}).unwrap()).unwrap(); + from_binary(&query(deps.as_ref(), env, QueryMsg::Config {}).unwrap()).unwrap(); assert_eq!(new_owner, config.owner); } diff --git a/contracts/tokenomics/staking/schema/config_response.json b/contracts/tokenomics/staking/schema/config_response.json new file mode 100644 index 00000000..251461bc --- /dev/null +++ b/contracts/tokenomics/staking/schema/config_response.json @@ -0,0 +1,33 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ConfigResponse", + "type": "object", + "required": [ + "deposit_token_addr", + "share_token_addr" + ], + "properties": { + "deposit_token_addr": { + "description": "The ASTRO token address", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + }, + "share_token_addr": { + "description": "The xASTRO token address", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + } + }, + "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/contracts/tokenomics/staking/schema/execute_msg.json b/contracts/tokenomics/staking/schema/execute_msg.json new file mode 100644 index 00000000..485ba8fa --- /dev/null +++ b/contracts/tokenomics/staking/schema/execute_msg.json @@ -0,0 +1,50 @@ +{ + "$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 + } + ], + "definitions": { + "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", + "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" + } + } + }, + "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/contracts/tokenomics/staking/schema/instantiate_msg.json b/contracts/tokenomics/staking/schema/instantiate_msg.json new file mode 100644 index 00000000..0f946bf8 --- /dev/null +++ b/contracts/tokenomics/staking/schema/instantiate_msg.json @@ -0,0 +1,27 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "InstantiateMsg", + "description": "This structure describes the parameters used for creating a contract.", + "type": "object", + "required": [ + "deposit_token_addr", + "owner", + "token_code_id" + ], + "properties": { + "deposit_token_addr": { + "description": "The ASTRO token contract address", + "type": "string" + }, + "owner": { + "description": "The contract owner address", + "type": "string" + }, + "token_code_id": { + "description": "CW20 token code identifier", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + } +} diff --git a/contracts/tokenomics/staking/schema/migrate_msg.json b/contracts/tokenomics/staking/schema/migrate_msg.json new file mode 100644 index 00000000..18c68d87 --- /dev/null +++ b/contracts/tokenomics/staking/schema/migrate_msg.json @@ -0,0 +1,6 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "MigrateMsg", + "description": "This structure describes a migration message. We currently take no arguments for migrations.", + "type": "object" +} diff --git a/contracts/tokenomics/staking/schema/query_msg.json b/contracts/tokenomics/staking/schema/query_msg.json new file mode 100644 index 00000000..ae7c946a --- /dev/null +++ b/contracts/tokenomics/staking/schema/query_msg.json @@ -0,0 +1,44 @@ +{ + "$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 the contract configuration specified in a custom [`ConfigResponse`] structure", + "type": "object", + "required": [ + "config" + ], + "properties": { + "config": { + "type": "object" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "total_shares" + ], + "properties": { + "total_shares": { + "type": "object" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "total_deposit" + ], + "properties": { + "total_deposit": { + "type": "object" + } + }, + "additionalProperties": false + } + ] +} diff --git a/contracts/tokenomics/vesting/schema/config_response.json b/contracts/tokenomics/vesting/schema/config_response.json new file mode 100644 index 00000000..74266b22 --- /dev/null +++ b/contracts/tokenomics/vesting/schema/config_response.json @@ -0,0 +1,34 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ConfigResponse", + "description": "This structure describes a custom struct used to return the contract configuration.", + "type": "object", + "required": [ + "owner", + "token_addr" + ], + "properties": { + "owner": { + "description": "Address allowed to set contract parameters", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + }, + "token_addr": { + "description": "The address of the token being vested", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + } + }, + "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/contracts/tokenomics/vesting/schema/execute_msg.json b/contracts/tokenomics/vesting/schema/execute_msg.json new file mode 100644 index 00000000..b0c39d77 --- /dev/null +++ b/contracts/tokenomics/vesting/schema/execute_msg.json @@ -0,0 +1,138 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ExecuteMsg", + "description": "This structure describes the execute messages available in the contract.", + "oneOf": [ + { + "description": "Claim claims vested tokens and sends them to a recipient", + "type": "object", + "required": [ + "claim" + ], + "properties": { + "claim": { + "type": "object", + "properties": { + "amount": { + "description": "The amount of tokens to claim", + "anyOf": [ + { + "$ref": "#/definitions/Uint128" + }, + { + "type": "null" + } + ] + }, + "recipient": { + "description": "The address that receives the vested tokens", + "type": [ + "string", + "null" + ] + } + } + } + }, + "additionalProperties": false + }, + { + "description": "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": "Creates a request to change contract ownership ## Executor Only the current owner can execute this", + "type": "object", + "required": [ + "propose_new_owner" + ], + "properties": { + "propose_new_owner": { + "type": "object", + "required": [ + "expires_in", + "owner" + ], + "properties": { + "expires_in": { + "description": "The validity period of the offer to change the owner", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "owner": { + "description": "The newly proposed owner", + "type": "string" + } + } + } + }, + "additionalProperties": false + }, + { + "description": "Removes a request to change contract ownership ## Executor Only the current owner can execute this", + "type": "object", + "required": [ + "drop_ownership_proposal" + ], + "properties": { + "drop_ownership_proposal": { + "type": "object" + } + }, + "additionalProperties": false + }, + { + "description": "Claims contract ownership ## Executor Only the newly proposed owner can execute this", + "type": "object", + "required": [ + "claim_ownership" + ], + "properties": { + "claim_ownership": { + "type": "object" + } + }, + "additionalProperties": false + } + ], + "definitions": { + "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", + "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" + } + } + }, + "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/contracts/tokenomics/vesting/schema/instantiate_msg.json b/contracts/tokenomics/vesting/schema/instantiate_msg.json new file mode 100644 index 00000000..f071bcc4 --- /dev/null +++ b/contracts/tokenomics/vesting/schema/instantiate_msg.json @@ -0,0 +1,20 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "InstantiateMsg", + "description": "This structure describes the parameters used for creating a contract.", + "type": "object", + "required": [ + "owner", + "token_addr" + ], + "properties": { + "owner": { + "description": "Address allowed to change contract parameters", + "type": "string" + }, + "token_addr": { + "description": "The address of the token that's being vested", + "type": "string" + } + } +} diff --git a/contracts/tokenomics/vesting/schema/migrate_msg.json b/contracts/tokenomics/vesting/schema/migrate_msg.json new file mode 100644 index 00000000..18c68d87 --- /dev/null +++ b/contracts/tokenomics/vesting/schema/migrate_msg.json @@ -0,0 +1,6 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "MigrateMsg", + "description": "This structure describes a migration message. We currently take no arguments for migrations.", + "type": "object" +} diff --git a/contracts/tokenomics/vesting/schema/query_msg.json b/contracts/tokenomics/vesting/schema/query_msg.json new file mode 100644 index 00000000..89ecefa7 --- /dev/null +++ b/contracts/tokenomics/vesting/schema/query_msg.json @@ -0,0 +1,128 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "QueryMsg", + "description": "This structure describes the query messages available in the contract.", + "oneOf": [ + { + "title": "Description", + "description": "Returns the configuration for the contract using a [`ConfigResponse`] object.", + "type": "object", + "required": [ + "config" + ], + "properties": { + "config": { + "type": "object" + } + }, + "additionalProperties": false + }, + { + "title": "Description", + "description": "Returns information about an address vesting tokens using a [`VestingAccountResponse`] object.", + "type": "object", + "required": [ + "vesting_account" + ], + "properties": { + "vesting_account": { + "type": "object", + "required": [ + "address" + ], + "properties": { + "address": { + "type": "string" + } + } + } + }, + "additionalProperties": false + }, + { + "title": "Description", + "description": "Returns a list of addresses that are vesting tokens using a [`VestingAccountsResponse`] object.", + "type": "object", + "required": [ + "vesting_accounts" + ], + "properties": { + "vesting_accounts": { + "type": "object", + "properties": { + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "order_by": { + "anyOf": [ + { + "$ref": "#/definitions/OrderBy" + }, + { + "type": "null" + } + ] + }, + "start_after": { + "type": [ + "string", + "null" + ] + } + } + } + }, + "additionalProperties": false + }, + { + "title": "Description", + "description": "Returns the total unvested amount of tokens for a specific address.", + "type": "object", + "required": [ + "available_amount" + ], + "properties": { + "available_amount": { + "type": "object", + "required": [ + "address" + ], + "properties": { + "address": { + "type": "string" + } + } + } + }, + "additionalProperties": false + }, + { + "description": "Timestamp returns the current timestamp", + "type": "object", + "required": [ + "timestamp" + ], + "properties": { + "timestamp": { + "type": "object" + } + }, + "additionalProperties": false + } + ], + "definitions": { + "OrderBy": { + "description": "This enum describes the types of sorting that can be applied to some piece of data", + "type": "string", + "enum": [ + "asc", + "desc" + ] + } + } +} diff --git a/contracts/tokenomics/vesting/schema/vesting_account_response.json b/contracts/tokenomics/vesting/schema/vesting_account_response.json new file mode 100644 index 00000000..2e09a07d --- /dev/null +++ b/contracts/tokenomics/vesting/schema/vesting_account_response.json @@ -0,0 +1,115 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "VestingAccountResponse", + "description": "This structure describes a custom struct used to return vesting data about a specific vesting target.", + "type": "object", + "required": [ + "address", + "info" + ], + "properties": { + "address": { + "description": "The address that's vesting tokens", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + }, + "info": { + "description": "Vesting information", + "allOf": [ + { + "$ref": "#/definitions/VestingInfo" + } + ] + } + }, + "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" + }, + "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" + }, + "VestingInfo": { + "description": "This structure stores parameters for a batch of vesting schedules.", + "type": "object", + "required": [ + "released_amount", + "schedules" + ], + "properties": { + "released_amount": { + "description": "The total amount of ASTRO already claimed", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] + }, + "schedules": { + "description": "The vesting schedules", + "type": "array", + "items": { + "$ref": "#/definitions/VestingSchedule" + } + } + } + }, + "VestingSchedule": { + "description": "This structure stores parameters for a specific vesting schedule", + "type": "object", + "required": [ + "start_point" + ], + "properties": { + "end_point": { + "description": "The end point for the vesting schedule", + "anyOf": [ + { + "$ref": "#/definitions/VestingSchedulePoint" + }, + { + "type": "null" + } + ] + }, + "start_point": { + "description": "The start date for the vesting schedule", + "allOf": [ + { + "$ref": "#/definitions/VestingSchedulePoint" + } + ] + } + } + }, + "VestingSchedulePoint": { + "description": "This structure stores the parameters used to create a vesting schedule.", + "type": "object", + "required": [ + "amount", + "time" + ], + "properties": { + "amount": { + "description": "The amount of tokens being vested", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] + }, + "time": { + "description": "The start time for the vesting schedule", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + } + } + } +} diff --git a/contracts/tokenomics/vesting/schema/vesting_accounts_response.json b/contracts/tokenomics/vesting/schema/vesting_accounts_response.json new file mode 100644 index 00000000..88ce1194 --- /dev/null +++ b/contracts/tokenomics/vesting/schema/vesting_accounts_response.json @@ -0,0 +1,131 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "VestingAccountsResponse", + "description": "This structure describes a custom struct used to return vesting data for multiple vesting targets.", + "type": "object", + "required": [ + "vesting_accounts" + ], + "properties": { + "vesting_accounts": { + "description": "A list of accounts that are vesting tokens", + "type": "array", + "items": { + "$ref": "#/definitions/VestingAccountResponse" + } + } + }, + "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" + }, + "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" + }, + "VestingAccountResponse": { + "description": "This structure describes a custom struct used to return vesting data about a specific vesting target.", + "type": "object", + "required": [ + "address", + "info" + ], + "properties": { + "address": { + "description": "The address that's vesting tokens", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + }, + "info": { + "description": "Vesting information", + "allOf": [ + { + "$ref": "#/definitions/VestingInfo" + } + ] + } + } + }, + "VestingInfo": { + "description": "This structure stores parameters for a batch of vesting schedules.", + "type": "object", + "required": [ + "released_amount", + "schedules" + ], + "properties": { + "released_amount": { + "description": "The total amount of ASTRO already claimed", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] + }, + "schedules": { + "description": "The vesting schedules", + "type": "array", + "items": { + "$ref": "#/definitions/VestingSchedule" + } + } + } + }, + "VestingSchedule": { + "description": "This structure stores parameters for a specific vesting schedule", + "type": "object", + "required": [ + "start_point" + ], + "properties": { + "end_point": { + "description": "The end point for the vesting schedule", + "anyOf": [ + { + "$ref": "#/definitions/VestingSchedulePoint" + }, + { + "type": "null" + } + ] + }, + "start_point": { + "description": "The start date for the vesting schedule", + "allOf": [ + { + "$ref": "#/definitions/VestingSchedulePoint" + } + ] + } + } + }, + "VestingSchedulePoint": { + "description": "This structure stores the parameters used to create a vesting schedule.", + "type": "object", + "required": [ + "amount", + "time" + ], + "properties": { + "amount": { + "description": "The amount of tokens being vested", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] + }, + "time": { + "description": "The start time for the vesting schedule", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + } + } + } +} diff --git a/contracts/tokenomics/vesting/src/testing.rs b/contracts/tokenomics/vesting/src/testing.rs index a17c874a..07756b6e 100644 --- a/contracts/tokenomics/vesting/src/testing.rs +++ b/contracts/tokenomics/vesting/src/testing.rs @@ -14,7 +14,7 @@ fn proper_initialization() { }; let env = mock_env(); - let info = mock_info("addr0000", &vec![]); + let info = mock_info("addr0000", &[]); let _res = instantiate(deps.as_mut(), env.clone(), info, msg).unwrap(); assert_eq!( diff --git a/contracts/tokenomics/xastro_token/schema/all_accounts_response.json b/contracts/tokenomics/xastro_token/schema/all_accounts_response.json new file mode 100644 index 00000000..cea50fba --- /dev/null +++ b/contracts/tokenomics/xastro_token/schema/all_accounts_response.json @@ -0,0 +1,16 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "AllAccountsResponse", + "type": "object", + "required": [ + "accounts" + ], + "properties": { + "accounts": { + "type": "array", + "items": { + "type": "string" + } + } + } +} diff --git a/contracts/tokenomics/xastro_token/schema/all_allowances_response.json b/contracts/tokenomics/xastro_token/schema/all_allowances_response.json new file mode 100644 index 00000000..6bb2291c --- /dev/null +++ b/contracts/tokenomics/xastro_token/schema/all_allowances_response.json @@ -0,0 +1,99 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "AllAllowancesResponse", + "type": "object", + "required": [ + "allowances" + ], + "properties": { + "allowances": { + "type": "array", + "items": { + "$ref": "#/definitions/AllowanceInfo" + } + } + }, + "definitions": { + "AllowanceInfo": { + "type": "object", + "required": [ + "allowance", + "expires", + "spender" + ], + "properties": { + "allowance": { + "$ref": "#/definitions/Uint128" + }, + "expires": { + "$ref": "#/definitions/Expiration" + }, + "spender": { + "type": "string" + } + } + }, + "Expiration": { + "description": "Expiration represents a point in time when some event happens. It can compare with a BlockInfo and will return is_expired() == true once the condition is hit (and for every block in the future)", + "oneOf": [ + { + "description": "AtHeight will expire when `env.block.height` >= height", + "type": "object", + "required": [ + "at_height" + ], + "properties": { + "at_height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + { + "description": "AtTime will expire when `env.block.time` >= time", + "type": "object", + "required": [ + "at_time" + ], + "properties": { + "at_time": { + "$ref": "#/definitions/Timestamp" + } + }, + "additionalProperties": false + }, + { + "description": "Never will never expire. Used to express the empty variant", + "type": "object", + "required": [ + "never" + ], + "properties": { + "never": { + "type": "object" + } + }, + "additionalProperties": false + } + ] + }, + "Timestamp": { + "description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```", + "allOf": [ + { + "$ref": "#/definitions/Uint64" + } + ] + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + }, + "Uint64": { + "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```", + "type": "string" + } + } +} diff --git a/contracts/tokenomics/xastro_token/schema/allowance_response.json b/contracts/tokenomics/xastro_token/schema/allowance_response.json new file mode 100644 index 00000000..c4f98d6f --- /dev/null +++ b/contracts/tokenomics/xastro_token/schema/allowance_response.json @@ -0,0 +1,81 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "AllowanceResponse", + "type": "object", + "required": [ + "allowance", + "expires" + ], + "properties": { + "allowance": { + "$ref": "#/definitions/Uint128" + }, + "expires": { + "$ref": "#/definitions/Expiration" + } + }, + "definitions": { + "Expiration": { + "description": "Expiration represents a point in time when some event happens. It can compare with a BlockInfo and will return is_expired() == true once the condition is hit (and for every block in the future)", + "oneOf": [ + { + "description": "AtHeight will expire when `env.block.height` >= height", + "type": "object", + "required": [ + "at_height" + ], + "properties": { + "at_height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + { + "description": "AtTime will expire when `env.block.time` >= time", + "type": "object", + "required": [ + "at_time" + ], + "properties": { + "at_time": { + "$ref": "#/definitions/Timestamp" + } + }, + "additionalProperties": false + }, + { + "description": "Never will never expire. Used to express the empty variant", + "type": "object", + "required": [ + "never" + ], + "properties": { + "never": { + "type": "object" + } + }, + "additionalProperties": false + } + ] + }, + "Timestamp": { + "description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```", + "allOf": [ + { + "$ref": "#/definitions/Uint64" + } + ] + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + }, + "Uint64": { + "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```", + "type": "string" + } + } +} diff --git a/contracts/tokenomics/xastro_token/schema/balance_response.json b/contracts/tokenomics/xastro_token/schema/balance_response.json new file mode 100644 index 00000000..4e1a0be2 --- /dev/null +++ b/contracts/tokenomics/xastro_token/schema/balance_response.json @@ -0,0 +1,19 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "BalanceResponse", + "type": "object", + "required": [ + "balance" + ], + "properties": { + "balance": { + "$ref": "#/definitions/Uint128" + } + }, + "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/contracts/tokenomics/xastro_token/schema/execute_msg.json b/contracts/tokenomics/xastro_token/schema/execute_msg.json new file mode 100644 index 00000000..eda0f5dc --- /dev/null +++ b/contracts/tokenomics/xastro_token/schema/execute_msg.json @@ -0,0 +1,442 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ExecuteMsg", + "oneOf": [ + { + "description": "Transfer is a base message to move tokens to another account without triggering actions", + "type": "object", + "required": [ + "transfer" + ], + "properties": { + "transfer": { + "type": "object", + "required": [ + "amount", + "recipient" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "recipient": { + "type": "string" + } + } + } + }, + "additionalProperties": false + }, + { + "description": "Burn is a base message to destroy tokens forever", + "type": "object", + "required": [ + "burn" + ], + "properties": { + "burn": { + "type": "object", + "required": [ + "amount" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + } + } + } + }, + "additionalProperties": false + }, + { + "description": "Send is a base message to transfer tokens to a contract and trigger an action on the receiving contract.", + "type": "object", + "required": [ + "send" + ], + "properties": { + "send": { + "type": "object", + "required": [ + "amount", + "contract", + "msg" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "contract": { + "type": "string" + }, + "msg": { + "$ref": "#/definitions/Binary" + } + } + } + }, + "additionalProperties": false + }, + { + "description": "Only with \"approval\" extension. Allows spender to access an additional amount tokens from the owner's (env.sender) account. If expires is Some(), overwrites current allowance expiration with this one.", + "type": "object", + "required": [ + "increase_allowance" + ], + "properties": { + "increase_allowance": { + "type": "object", + "required": [ + "amount", + "spender" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "expires": { + "anyOf": [ + { + "$ref": "#/definitions/Expiration" + }, + { + "type": "null" + } + ] + }, + "spender": { + "type": "string" + } + } + } + }, + "additionalProperties": false + }, + { + "description": "Only with \"approval\" extension. Lowers the spender's access of tokens from the owner's (env.sender) account by amount. If expires is Some(), overwrites current allowance expiration with this one.", + "type": "object", + "required": [ + "decrease_allowance" + ], + "properties": { + "decrease_allowance": { + "type": "object", + "required": [ + "amount", + "spender" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "expires": { + "anyOf": [ + { + "$ref": "#/definitions/Expiration" + }, + { + "type": "null" + } + ] + }, + "spender": { + "type": "string" + } + } + } + }, + "additionalProperties": false + }, + { + "description": "Only with \"approval\" extension. Transfers amount tokens from owner -> recipient if `env.sender` has sufficient pre-approval.", + "type": "object", + "required": [ + "transfer_from" + ], + "properties": { + "transfer_from": { + "type": "object", + "required": [ + "amount", + "owner", + "recipient" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "owner": { + "type": "string" + }, + "recipient": { + "type": "string" + } + } + } + }, + "additionalProperties": false + }, + { + "description": "Only with \"approval\" extension. Sends amount tokens from owner -> contract if `env.sender` has sufficient pre-approval.", + "type": "object", + "required": [ + "send_from" + ], + "properties": { + "send_from": { + "type": "object", + "required": [ + "amount", + "contract", + "msg", + "owner" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "contract": { + "type": "string" + }, + "msg": { + "$ref": "#/definitions/Binary" + }, + "owner": { + "type": "string" + } + } + } + }, + "additionalProperties": false + }, + { + "description": "Only with \"approval\" extension. Destroys tokens forever", + "type": "object", + "required": [ + "burn_from" + ], + "properties": { + "burn_from": { + "type": "object", + "required": [ + "amount", + "owner" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "owner": { + "type": "string" + } + } + } + }, + "additionalProperties": false + }, + { + "description": "Only with the \"mintable\" extension. If authorized, creates amount new tokens and adds to the recipient balance.", + "type": "object", + "required": [ + "mint" + ], + "properties": { + "mint": { + "type": "object", + "required": [ + "amount", + "recipient" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "recipient": { + "type": "string" + } + } + } + }, + "additionalProperties": false + }, + { + "description": "Only with the \"marketing\" extension. If authorized, updates marketing metadata. Setting None/null for any of these will leave it unchanged. Setting Some(\"\") will clear this field on the contract storage", + "type": "object", + "required": [ + "update_marketing" + ], + "properties": { + "update_marketing": { + "type": "object", + "properties": { + "description": { + "description": "A longer description of the token and it's utility. Designed for tooltips or such", + "type": [ + "string", + "null" + ] + }, + "marketing": { + "description": "The address (if any) who can update this data structure", + "type": [ + "string", + "null" + ] + }, + "project": { + "description": "A URL pointing to the project behind this token.", + "type": [ + "string", + "null" + ] + } + } + } + }, + "additionalProperties": false + }, + { + "description": "If set as the \"marketing\" role on the contract, upload a new URL, SVG, or PNG for the token", + "type": "object", + "required": [ + "upload_logo" + ], + "properties": { + "upload_logo": { + "$ref": "#/definitions/Logo" + } + }, + "additionalProperties": false + } + ], + "definitions": { + "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", + "type": "string" + }, + "EmbeddedLogo": { + "description": "This is used to store the logo on the blockchain in an accepted format. Enforce maximum size of 5KB on all variants.", + "oneOf": [ + { + "description": "Store the Logo as an SVG file. The content must conform to the spec at https://en.wikipedia.org/wiki/Scalable_Vector_Graphics (The contract should do some light-weight sanity-check validation)", + "type": "object", + "required": [ + "svg" + ], + "properties": { + "svg": { + "$ref": "#/definitions/Binary" + } + }, + "additionalProperties": false + }, + { + "description": "Store the Logo as a PNG file. This will likely only support up to 64x64 or so within the 5KB limit.", + "type": "object", + "required": [ + "png" + ], + "properties": { + "png": { + "$ref": "#/definitions/Binary" + } + }, + "additionalProperties": false + } + ] + }, + "Expiration": { + "description": "Expiration represents a point in time when some event happens. It can compare with a BlockInfo and will return is_expired() == true once the condition is hit (and for every block in the future)", + "oneOf": [ + { + "description": "AtHeight will expire when `env.block.height` >= height", + "type": "object", + "required": [ + "at_height" + ], + "properties": { + "at_height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + { + "description": "AtTime will expire when `env.block.time` >= time", + "type": "object", + "required": [ + "at_time" + ], + "properties": { + "at_time": { + "$ref": "#/definitions/Timestamp" + } + }, + "additionalProperties": false + }, + { + "description": "Never will never expire. Used to express the empty variant", + "type": "object", + "required": [ + "never" + ], + "properties": { + "never": { + "type": "object" + } + }, + "additionalProperties": false + } + ] + }, + "Logo": { + "description": "This is used for uploading logo data, or setting it in InstantiateData", + "oneOf": [ + { + "description": "A reference to an externally hosted logo. Must be a valid HTTP or HTTPS URL.", + "type": "object", + "required": [ + "url" + ], + "properties": { + "url": { + "type": "string" + } + }, + "additionalProperties": false + }, + { + "description": "Logo content stored on the blockchain. Enforce maximum size of 5KB on all variants", + "type": "object", + "required": [ + "embedded" + ], + "properties": { + "embedded": { + "$ref": "#/definitions/EmbeddedLogo" + } + }, + "additionalProperties": false + } + ] + }, + "Timestamp": { + "description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```", + "allOf": [ + { + "$ref": "#/definitions/Uint64" + } + ] + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + }, + "Uint64": { + "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```", + "type": "string" + } + } +} diff --git a/contracts/tokenomics/xastro_token/schema/instantiate_msg.json b/contracts/tokenomics/xastro_token/schema/instantiate_msg.json new file mode 100644 index 00000000..965bad53 --- /dev/null +++ b/contracts/tokenomics/xastro_token/schema/instantiate_msg.json @@ -0,0 +1,89 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "InstantiateMsg", + "description": "This structure describes the parameters used for creating a xASTRO token contract.", + "type": "object", + "required": [ + "decimals", + "initial_balances", + "name", + "symbol" + ], + "properties": { + "decimals": { + "description": "The number of decimals the token has", + "type": "integer", + "format": "uint8", + "minimum": 0.0 + }, + "initial_balances": { + "description": "Initial token balances", + "type": "array", + "items": { + "$ref": "#/definitions/Cw20Coin" + } + }, + "mint": { + "description": "Token minting permissions", + "anyOf": [ + { + "$ref": "#/definitions/MinterResponse" + }, + { + "type": "null" + } + ] + }, + "name": { + "description": "Token name", + "type": "string" + }, + "symbol": { + "description": "Token symbol", + "type": "string" + } + }, + "definitions": { + "Cw20Coin": { + "type": "object", + "required": [ + "address", + "amount" + ], + "properties": { + "address": { + "type": "string" + }, + "amount": { + "$ref": "#/definitions/Uint128" + } + } + }, + "MinterResponse": { + "type": "object", + "required": [ + "minter" + ], + "properties": { + "cap": { + "description": "cap is a hard cap on total supply that can be achieved by minting. Note that this refers to total_supply. If None, there is unlimited cap.", + "anyOf": [ + { + "$ref": "#/definitions/Uint128" + }, + { + "type": "null" + } + ] + }, + "minter": { + "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/contracts/tokenomics/xastro_token/schema/minter_response.json b/contracts/tokenomics/xastro_token/schema/minter_response.json new file mode 100644 index 00000000..a05f8fd5 --- /dev/null +++ b/contracts/tokenomics/xastro_token/schema/minter_response.json @@ -0,0 +1,30 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "MinterResponse", + "type": "object", + "required": [ + "minter" + ], + "properties": { + "cap": { + "description": "cap is a hard cap on total supply that can be achieved by minting. Note that this refers to total_supply. If None, there is unlimited cap.", + "anyOf": [ + { + "$ref": "#/definitions/Uint128" + }, + { + "type": "null" + } + ] + }, + "minter": { + "type": "string" + } + }, + "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/contracts/tokenomics/xastro_token/schema/query_msg.json b/contracts/tokenomics/xastro_token/schema/query_msg.json new file mode 100644 index 00000000..0a5b4f08 --- /dev/null +++ b/contracts/tokenomics/xastro_token/schema/query_msg.json @@ -0,0 +1,219 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "QueryMsg", + "description": "This enum describes the query messages available in the contract.", + "oneOf": [ + { + "description": "Balance returns the current balance of a given address, 0 if unset.", + "type": "object", + "required": [ + "balance" + ], + "properties": { + "balance": { + "type": "object", + "required": [ + "address" + ], + "properties": { + "address": { + "type": "string" + } + } + } + }, + "additionalProperties": false + }, + { + "description": "BalanceAt returns balance of the given address at the given block, 0 if unset.", + "type": "object", + "required": [ + "balance_at" + ], + "properties": { + "balance_at": { + "type": "object", + "required": [ + "address", + "block" + ], + "properties": { + "address": { + "type": "string" + }, + "block": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + } + } + }, + "additionalProperties": false + }, + { + "description": "TotalSupplyAt returns the total token supply at the given block.", + "type": "object", + "required": [ + "total_supply_at" + ], + "properties": { + "total_supply_at": { + "type": "object", + "required": [ + "block" + ], + "properties": { + "block": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + } + } + }, + "additionalProperties": false + }, + { + "description": "TokenInfo returns the contract's metadata - name, decimals, supply, etc.", + "type": "object", + "required": [ + "token_info" + ], + "properties": { + "token_info": { + "type": "object" + } + }, + "additionalProperties": false + }, + { + "description": "Returns who can mint xASTRO and the hard cap on maximum tokens after minting.", + "type": "object", + "required": [ + "minter" + ], + "properties": { + "minter": { + "type": "object" + } + }, + "additionalProperties": false + }, + { + "description": "Allowance returns an amount of tokens the spender can spend from the owner account, 0 if unset.", + "type": "object", + "required": [ + "allowance" + ], + "properties": { + "allowance": { + "type": "object", + "required": [ + "owner", + "spender" + ], + "properties": { + "owner": { + "type": "string" + }, + "spender": { + "type": "string" + } + } + } + }, + "additionalProperties": false + }, + { + "description": "AllAllowances returns all the allowances this token holder has approved. Supports pagination.", + "type": "object", + "required": [ + "all_allowances" + ], + "properties": { + "all_allowances": { + "type": "object", + "required": [ + "owner" + ], + "properties": { + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "owner": { + "type": "string" + }, + "start_after": { + "type": [ + "string", + "null" + ] + } + } + } + }, + "additionalProperties": false + }, + { + "description": "AllAccounts returns all the accounts that have xASTRO balances. Supports pagination.", + "type": "object", + "required": [ + "all_accounts" + ], + "properties": { + "all_accounts": { + "type": "object", + "properties": { + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "type": [ + "string", + "null" + ] + } + } + } + }, + "additionalProperties": false + }, + { + "description": "Returns marketing related contract metadata: - description, logo, project url, etc.", + "type": "object", + "required": [ + "marketing_info" + ], + "properties": { + "marketing_info": { + "type": "object" + } + }, + "additionalProperties": false + }, + { + "description": "Downloads embeded logo data (if stored on chain). Errors if no logo data was stored for this contract.", + "type": "object", + "required": [ + "download_logo" + ], + "properties": { + "download_logo": { + "type": "object" + } + }, + "additionalProperties": false + } + ] +} diff --git a/contracts/tokenomics/xastro_token/schema/token_info_response.json b/contracts/tokenomics/xastro_token/schema/token_info_response.json new file mode 100644 index 00000000..9920c841 --- /dev/null +++ b/contracts/tokenomics/xastro_token/schema/token_info_response.json @@ -0,0 +1,33 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "TokenInfoResponse", + "type": "object", + "required": [ + "decimals", + "name", + "symbol", + "total_supply" + ], + "properties": { + "decimals": { + "type": "integer", + "format": "uint8", + "minimum": 0.0 + }, + "name": { + "type": "string" + }, + "symbol": { + "type": "string" + }, + "total_supply": { + "$ref": "#/definitions/Uint128" + } + }, + "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/contracts/tokenomics/xastro_token/src/testing.rs b/contracts/tokenomics/xastro_token/src/testing.rs index 5fa8c277..edac887c 100644 --- a/contracts/tokenomics/xastro_token/src/testing.rs +++ b/contracts/tokenomics/xastro_token/src/testing.rs @@ -214,7 +214,7 @@ mod instantiate { let err = instantiate(deps.as_mut(), env, info, instantiate_msg).unwrap_err(); assert_eq!( err, - StdError::generic_err("Initial supply greater than cap").into() + StdError::generic_err("Initial supply greater than cap") ); } } @@ -613,7 +613,7 @@ fn snapshots_are_taken_and_retrieved_correctly() { current_block += 60_000; let transfer_amount = Uint128::new(10_000); - current_addr1_balance = current_addr1_balance - transfer_amount; + current_addr1_balance -= transfer_amount; current_addr2_balance += transfer_amount; let info = mock_info(addr1.as_str(), &[]); @@ -638,8 +638,8 @@ fn snapshots_are_taken_and_retrieved_correctly() { current_block += 50_000; let burn_amount = Uint128::new(20_000); - current_total_supply = current_total_supply - burn_amount; - current_addr2_balance = current_addr2_balance - burn_amount; + current_total_supply -= burn_amount; + current_addr2_balance -= burn_amount; let info = mock_info(addr2.as_str(), &[]); diff --git a/contracts/whitelist/schema/admin_list_response.json b/contracts/whitelist/schema/admin_list_response.json new file mode 100644 index 00000000..bc20467c --- /dev/null +++ b/contracts/whitelist/schema/admin_list_response.json @@ -0,0 +1,20 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "AdminListResponse", + "type": "object", + "required": [ + "admins", + "mutable" + ], + "properties": { + "admins": { + "type": "array", + "items": { + "type": "string" + } + }, + "mutable": { + "type": "boolean" + } + } +} diff --git a/contracts/whitelist/schema/execute_msg.json b/contracts/whitelist/schema/execute_msg.json new file mode 100644 index 00000000..a1487c21 --- /dev/null +++ b/contracts/whitelist/schema/execute_msg.json @@ -0,0 +1,525 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ExecuteMsg", + "oneOf": [ + { + "description": "Execute contract requests to re-dispatch messages with the contract's address as a sender. Every implementation has its own logic", + "type": "object", + "required": [ + "execute" + ], + "properties": { + "execute": { + "type": "object", + "required": [ + "msgs" + ], + "properties": { + "msgs": { + "type": "array", + "items": { + "$ref": "#/definitions/CosmosMsg_for_Empty" + } + } + } + } + }, + "additionalProperties": false + }, + { + "description": "Freeze will make a mutable contract immutable, must be called by an admin", + "type": "object", + "required": [ + "freeze" + ], + "properties": { + "freeze": { + "type": "object" + } + }, + "additionalProperties": false + }, + { + "description": "UpdateAdmins will change the admin set of the contract, must be called by an existing admin, and only works if the contract is mutable", + "type": "object", + "required": [ + "update_admins" + ], + "properties": { + "update_admins": { + "type": "object", + "required": [ + "admins" + ], + "properties": { + "admins": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + }, + "additionalProperties": false + } + ], + "definitions": { + "BankMsg": { + "description": "The message types of the bank module.\n\nSee https://github.com/cosmos/cosmos-sdk/blob/v0.40.0/proto/cosmos/bank/v1beta1/tx.proto", + "oneOf": [ + { + "description": "Sends native tokens from the contract to the given address.\n\nThis is translated to a [MsgSend](https://github.com/cosmos/cosmos-sdk/blob/v0.40.0/proto/cosmos/bank/v1beta1/tx.proto#L19-L28). `from_address` is automatically filled with the current contract's address.", + "type": "object", + "required": [ + "send" + ], + "properties": { + "send": { + "type": "object", + "required": [ + "amount", + "to_address" + ], + "properties": { + "amount": { + "type": "array", + "items": { + "$ref": "#/definitions/Coin" + } + }, + "to_address": { + "type": "string" + } + } + } + }, + "additionalProperties": false + }, + { + "description": "This will burn the given coins from the contract's account. There is no Cosmos SDK message that performs this, but it can be done by calling the bank keeper. Important if a contract controls significant token supply that must be retired.", + "type": "object", + "required": [ + "burn" + ], + "properties": { + "burn": { + "type": "object", + "required": [ + "amount" + ], + "properties": { + "amount": { + "type": "array", + "items": { + "$ref": "#/definitions/Coin" + } + } + } + } + }, + "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", + "type": "string" + }, + "Coin": { + "type": "object", + "required": [ + "amount", + "denom" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "denom": { + "type": "string" + } + } + }, + "CosmosMsg_for_Empty": { + "oneOf": [ + { + "type": "object", + "required": [ + "bank" + ], + "properties": { + "bank": { + "$ref": "#/definitions/BankMsg" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "custom" + ], + "properties": { + "custom": { + "$ref": "#/definitions/Empty" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "staking" + ], + "properties": { + "staking": { + "$ref": "#/definitions/StakingMsg" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "distribution" + ], + "properties": { + "distribution": { + "$ref": "#/definitions/DistributionMsg" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "wasm" + ], + "properties": { + "wasm": { + "$ref": "#/definitions/WasmMsg" + } + }, + "additionalProperties": false + } + ] + }, + "DistributionMsg": { + "description": "The message types of the distribution module.\n\nSee https://github.com/cosmos/cosmos-sdk/blob/v0.42.4/proto/cosmos/distribution/v1beta1/tx.proto", + "oneOf": [ + { + "description": "This is translated to a [MsgSetWithdrawAddress](https://github.com/cosmos/cosmos-sdk/blob/v0.42.4/proto/cosmos/distribution/v1beta1/tx.proto#L29-L37). `delegator_address` is automatically filled with the current contract's address.", + "type": "object", + "required": [ + "set_withdraw_address" + ], + "properties": { + "set_withdraw_address": { + "type": "object", + "required": [ + "address" + ], + "properties": { + "address": { + "description": "The `withdraw_address`", + "type": "string" + } + } + } + }, + "additionalProperties": false + }, + { + "description": "This is translated to a [[MsgWithdrawDelegatorReward](https://github.com/cosmos/cosmos-sdk/blob/v0.42.4/proto/cosmos/distribution/v1beta1/tx.proto#L42-L50). `delegator_address` is automatically filled with the current contract's address.", + "type": "object", + "required": [ + "withdraw_delegator_reward" + ], + "properties": { + "withdraw_delegator_reward": { + "type": "object", + "required": [ + "validator" + ], + "properties": { + "validator": { + "description": "The `validator_address`", + "type": "string" + } + } + } + }, + "additionalProperties": false + } + ] + }, + "Empty": { + "description": "An empty struct that serves as a placeholder in different places, such as contracts that don't set a custom message.\n\nIt is designed to be expressable in correct JSON and JSON Schema but contains no meaningful data. Previously we used enums without cases, but those cannot represented as valid JSON Schema (https://github.com/CosmWasm/cosmwasm/issues/451)", + "type": "object" + }, + "StakingMsg": { + "description": "The message types of the staking module.\n\nSee https://github.com/cosmos/cosmos-sdk/blob/v0.40.0/proto/cosmos/staking/v1beta1/tx.proto", + "oneOf": [ + { + "description": "This is translated to a [MsgDelegate](https://github.com/cosmos/cosmos-sdk/blob/v0.40.0/proto/cosmos/staking/v1beta1/tx.proto#L81-L90). `delegator_address` is automatically filled with the current contract's address.", + "type": "object", + "required": [ + "delegate" + ], + "properties": { + "delegate": { + "type": "object", + "required": [ + "amount", + "validator" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Coin" + }, + "validator": { + "type": "string" + } + } + } + }, + "additionalProperties": false + }, + { + "description": "This is translated to a [MsgUndelegate](https://github.com/cosmos/cosmos-sdk/blob/v0.40.0/proto/cosmos/staking/v1beta1/tx.proto#L112-L121). `delegator_address` is automatically filled with the current contract's address.", + "type": "object", + "required": [ + "undelegate" + ], + "properties": { + "undelegate": { + "type": "object", + "required": [ + "amount", + "validator" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Coin" + }, + "validator": { + "type": "string" + } + } + } + }, + "additionalProperties": false + }, + { + "description": "This is translated to a [MsgBeginRedelegate](https://github.com/cosmos/cosmos-sdk/blob/v0.40.0/proto/cosmos/staking/v1beta1/tx.proto#L95-L105). `delegator_address` is automatically filled with the current contract's address.", + "type": "object", + "required": [ + "redelegate" + ], + "properties": { + "redelegate": { + "type": "object", + "required": [ + "amount", + "dst_validator", + "src_validator" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Coin" + }, + "dst_validator": { + "type": "string" + }, + "src_validator": { + "type": "string" + } + } + } + }, + "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" + }, + "WasmMsg": { + "description": "The message types of the wasm module.\n\nSee https://github.com/CosmWasm/wasmd/blob/v0.14.0/x/wasm/internal/types/tx.proto", + "oneOf": [ + { + "description": "Dispatches a call to another contract at a known address (with known ABI).\n\nThis is translated to a [MsgExecuteContract](https://github.com/CosmWasm/wasmd/blob/v0.14.0/x/wasm/internal/types/tx.proto#L68-L78). `sender` is automatically filled with the current contract's address.", + "type": "object", + "required": [ + "execute" + ], + "properties": { + "execute": { + "type": "object", + "required": [ + "contract_addr", + "funds", + "msg" + ], + "properties": { + "contract_addr": { + "type": "string" + }, + "funds": { + "type": "array", + "items": { + "$ref": "#/definitions/Coin" + } + }, + "msg": { + "description": "msg is the json-encoded ExecuteMsg struct (as raw Binary)", + "allOf": [ + { + "$ref": "#/definitions/Binary" + } + ] + } + } + } + }, + "additionalProperties": false + }, + { + "description": "Instantiates a new contracts from previously uploaded Wasm code.\n\nThis is translated to a [MsgInstantiateContract](https://github.com/CosmWasm/wasmd/blob/v0.16.0-alpha1/x/wasm/internal/types/tx.proto#L47-L61). `sender` is automatically filled with the current contract's address.", + "type": "object", + "required": [ + "instantiate" + ], + "properties": { + "instantiate": { + "type": "object", + "required": [ + "code_id", + "funds", + "label", + "msg" + ], + "properties": { + "admin": { + "type": [ + "string", + "null" + ] + }, + "code_id": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "funds": { + "type": "array", + "items": { + "$ref": "#/definitions/Coin" + } + }, + "label": { + "description": "A human-readbale label for the contract", + "type": "string" + }, + "msg": { + "description": "msg is the JSON-encoded InstantiateMsg struct (as raw Binary)", + "allOf": [ + { + "$ref": "#/definitions/Binary" + } + ] + } + } + } + }, + "additionalProperties": false + }, + { + "description": "Migrates a given contracts to use new wasm code. Passes a MigrateMsg to allow us to customize behavior.\n\nOnly the contract admin (as defined in wasmd), if any, is able to make this call.\n\nThis is translated to a [MsgMigrateContract](https://github.com/CosmWasm/wasmd/blob/v0.14.0/x/wasm/internal/types/tx.proto#L86-L96). `sender` is automatically filled with the current contract's address.", + "type": "object", + "required": [ + "migrate" + ], + "properties": { + "migrate": { + "type": "object", + "required": [ + "contract_addr", + "msg", + "new_code_id" + ], + "properties": { + "contract_addr": { + "type": "string" + }, + "msg": { + "description": "msg is the json-encoded MigrateMsg struct that will be passed to the new code", + "allOf": [ + { + "$ref": "#/definitions/Binary" + } + ] + }, + "new_code_id": { + "description": "the code_id of the new logic to place in the given contract", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + } + } + }, + "additionalProperties": false + }, + { + "description": "Sets a new admin (for migrate) on the given contract. Fails if this contract is not currently admin of the target contract.", + "type": "object", + "required": [ + "update_admin" + ], + "properties": { + "update_admin": { + "type": "object", + "required": [ + "admin", + "contract_addr" + ], + "properties": { + "admin": { + "type": "string" + }, + "contract_addr": { + "type": "string" + } + } + } + }, + "additionalProperties": false + }, + { + "description": "Clears the admin on the given contract, so no more migration possible. Fails if this contract is not currently admin of the target contract.", + "type": "object", + "required": [ + "clear_admin" + ], + "properties": { + "clear_admin": { + "type": "object", + "required": [ + "contract_addr" + ], + "properties": { + "contract_addr": { + "type": "string" + } + } + } + }, + "additionalProperties": false + } + ] + } + } +} diff --git a/contracts/whitelist/schema/instantiate_msg.json b/contracts/whitelist/schema/instantiate_msg.json new file mode 100644 index 00000000..fdcd684a --- /dev/null +++ b/contracts/whitelist/schema/instantiate_msg.json @@ -0,0 +1,20 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "InstantiateMsg", + "type": "object", + "required": [ + "admins", + "mutable" + ], + "properties": { + "admins": { + "type": "array", + "items": { + "type": "string" + } + }, + "mutable": { + "type": "boolean" + } + } +} diff --git a/contracts/whitelist/schema/query_msg.json b/contracts/whitelist/schema/query_msg.json new file mode 100644 index 00000000..ff3d7793 --- /dev/null +++ b/contracts/whitelist/schema/query_msg.json @@ -0,0 +1,502 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "QueryMsg", + "oneOf": [ + { + "description": "Shows all admins and whether or not the contract is mutable", + "type": "object", + "required": [ + "admin_list" + ], + "properties": { + "admin_list": { + "type": "object" + } + }, + "additionalProperties": false + }, + { + "description": "Checks caller permissions on this proxy. If CanExecute returns true, then a call to `Execute` with the same message, before any further state changes, should also succeed.", + "type": "object", + "required": [ + "can_execute" + ], + "properties": { + "can_execute": { + "type": "object", + "required": [ + "msg", + "sender" + ], + "properties": { + "msg": { + "$ref": "#/definitions/CosmosMsg_for_Empty" + }, + "sender": { + "type": "string" + } + } + } + }, + "additionalProperties": false + } + ], + "definitions": { + "BankMsg": { + "description": "The message types of the bank module.\n\nSee https://github.com/cosmos/cosmos-sdk/blob/v0.40.0/proto/cosmos/bank/v1beta1/tx.proto", + "oneOf": [ + { + "description": "Sends native tokens from the contract to the given address.\n\nThis is translated to a [MsgSend](https://github.com/cosmos/cosmos-sdk/blob/v0.40.0/proto/cosmos/bank/v1beta1/tx.proto#L19-L28). `from_address` is automatically filled with the current contract's address.", + "type": "object", + "required": [ + "send" + ], + "properties": { + "send": { + "type": "object", + "required": [ + "amount", + "to_address" + ], + "properties": { + "amount": { + "type": "array", + "items": { + "$ref": "#/definitions/Coin" + } + }, + "to_address": { + "type": "string" + } + } + } + }, + "additionalProperties": false + }, + { + "description": "This will burn the given coins from the contract's account. There is no Cosmos SDK message that performs this, but it can be done by calling the bank keeper. Important if a contract controls significant token supply that must be retired.", + "type": "object", + "required": [ + "burn" + ], + "properties": { + "burn": { + "type": "object", + "required": [ + "amount" + ], + "properties": { + "amount": { + "type": "array", + "items": { + "$ref": "#/definitions/Coin" + } + } + } + } + }, + "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", + "type": "string" + }, + "Coin": { + "type": "object", + "required": [ + "amount", + "denom" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "denom": { + "type": "string" + } + } + }, + "CosmosMsg_for_Empty": { + "oneOf": [ + { + "type": "object", + "required": [ + "bank" + ], + "properties": { + "bank": { + "$ref": "#/definitions/BankMsg" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "custom" + ], + "properties": { + "custom": { + "$ref": "#/definitions/Empty" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "staking" + ], + "properties": { + "staking": { + "$ref": "#/definitions/StakingMsg" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "distribution" + ], + "properties": { + "distribution": { + "$ref": "#/definitions/DistributionMsg" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "wasm" + ], + "properties": { + "wasm": { + "$ref": "#/definitions/WasmMsg" + } + }, + "additionalProperties": false + } + ] + }, + "DistributionMsg": { + "description": "The message types of the distribution module.\n\nSee https://github.com/cosmos/cosmos-sdk/blob/v0.42.4/proto/cosmos/distribution/v1beta1/tx.proto", + "oneOf": [ + { + "description": "This is translated to a [MsgSetWithdrawAddress](https://github.com/cosmos/cosmos-sdk/blob/v0.42.4/proto/cosmos/distribution/v1beta1/tx.proto#L29-L37). `delegator_address` is automatically filled with the current contract's address.", + "type": "object", + "required": [ + "set_withdraw_address" + ], + "properties": { + "set_withdraw_address": { + "type": "object", + "required": [ + "address" + ], + "properties": { + "address": { + "description": "The `withdraw_address`", + "type": "string" + } + } + } + }, + "additionalProperties": false + }, + { + "description": "This is translated to a [[MsgWithdrawDelegatorReward](https://github.com/cosmos/cosmos-sdk/blob/v0.42.4/proto/cosmos/distribution/v1beta1/tx.proto#L42-L50). `delegator_address` is automatically filled with the current contract's address.", + "type": "object", + "required": [ + "withdraw_delegator_reward" + ], + "properties": { + "withdraw_delegator_reward": { + "type": "object", + "required": [ + "validator" + ], + "properties": { + "validator": { + "description": "The `validator_address`", + "type": "string" + } + } + } + }, + "additionalProperties": false + } + ] + }, + "Empty": { + "description": "An empty struct that serves as a placeholder in different places, such as contracts that don't set a custom message.\n\nIt is designed to be expressable in correct JSON and JSON Schema but contains no meaningful data. Previously we used enums without cases, but those cannot represented as valid JSON Schema (https://github.com/CosmWasm/cosmwasm/issues/451)", + "type": "object" + }, + "StakingMsg": { + "description": "The message types of the staking module.\n\nSee https://github.com/cosmos/cosmos-sdk/blob/v0.40.0/proto/cosmos/staking/v1beta1/tx.proto", + "oneOf": [ + { + "description": "This is translated to a [MsgDelegate](https://github.com/cosmos/cosmos-sdk/blob/v0.40.0/proto/cosmos/staking/v1beta1/tx.proto#L81-L90). `delegator_address` is automatically filled with the current contract's address.", + "type": "object", + "required": [ + "delegate" + ], + "properties": { + "delegate": { + "type": "object", + "required": [ + "amount", + "validator" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Coin" + }, + "validator": { + "type": "string" + } + } + } + }, + "additionalProperties": false + }, + { + "description": "This is translated to a [MsgUndelegate](https://github.com/cosmos/cosmos-sdk/blob/v0.40.0/proto/cosmos/staking/v1beta1/tx.proto#L112-L121). `delegator_address` is automatically filled with the current contract's address.", + "type": "object", + "required": [ + "undelegate" + ], + "properties": { + "undelegate": { + "type": "object", + "required": [ + "amount", + "validator" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Coin" + }, + "validator": { + "type": "string" + } + } + } + }, + "additionalProperties": false + }, + { + "description": "This is translated to a [MsgBeginRedelegate](https://github.com/cosmos/cosmos-sdk/blob/v0.40.0/proto/cosmos/staking/v1beta1/tx.proto#L95-L105). `delegator_address` is automatically filled with the current contract's address.", + "type": "object", + "required": [ + "redelegate" + ], + "properties": { + "redelegate": { + "type": "object", + "required": [ + "amount", + "dst_validator", + "src_validator" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Coin" + }, + "dst_validator": { + "type": "string" + }, + "src_validator": { + "type": "string" + } + } + } + }, + "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" + }, + "WasmMsg": { + "description": "The message types of the wasm module.\n\nSee https://github.com/CosmWasm/wasmd/blob/v0.14.0/x/wasm/internal/types/tx.proto", + "oneOf": [ + { + "description": "Dispatches a call to another contract at a known address (with known ABI).\n\nThis is translated to a [MsgExecuteContract](https://github.com/CosmWasm/wasmd/blob/v0.14.0/x/wasm/internal/types/tx.proto#L68-L78). `sender` is automatically filled with the current contract's address.", + "type": "object", + "required": [ + "execute" + ], + "properties": { + "execute": { + "type": "object", + "required": [ + "contract_addr", + "funds", + "msg" + ], + "properties": { + "contract_addr": { + "type": "string" + }, + "funds": { + "type": "array", + "items": { + "$ref": "#/definitions/Coin" + } + }, + "msg": { + "description": "msg is the json-encoded ExecuteMsg struct (as raw Binary)", + "allOf": [ + { + "$ref": "#/definitions/Binary" + } + ] + } + } + } + }, + "additionalProperties": false + }, + { + "description": "Instantiates a new contracts from previously uploaded Wasm code.\n\nThis is translated to a [MsgInstantiateContract](https://github.com/CosmWasm/wasmd/blob/v0.16.0-alpha1/x/wasm/internal/types/tx.proto#L47-L61). `sender` is automatically filled with the current contract's address.", + "type": "object", + "required": [ + "instantiate" + ], + "properties": { + "instantiate": { + "type": "object", + "required": [ + "code_id", + "funds", + "label", + "msg" + ], + "properties": { + "admin": { + "type": [ + "string", + "null" + ] + }, + "code_id": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "funds": { + "type": "array", + "items": { + "$ref": "#/definitions/Coin" + } + }, + "label": { + "description": "A human-readbale label for the contract", + "type": "string" + }, + "msg": { + "description": "msg is the JSON-encoded InstantiateMsg struct (as raw Binary)", + "allOf": [ + { + "$ref": "#/definitions/Binary" + } + ] + } + } + } + }, + "additionalProperties": false + }, + { + "description": "Migrates a given contracts to use new wasm code. Passes a MigrateMsg to allow us to customize behavior.\n\nOnly the contract admin (as defined in wasmd), if any, is able to make this call.\n\nThis is translated to a [MsgMigrateContract](https://github.com/CosmWasm/wasmd/blob/v0.14.0/x/wasm/internal/types/tx.proto#L86-L96). `sender` is automatically filled with the current contract's address.", + "type": "object", + "required": [ + "migrate" + ], + "properties": { + "migrate": { + "type": "object", + "required": [ + "contract_addr", + "msg", + "new_code_id" + ], + "properties": { + "contract_addr": { + "type": "string" + }, + "msg": { + "description": "msg is the json-encoded MigrateMsg struct that will be passed to the new code", + "allOf": [ + { + "$ref": "#/definitions/Binary" + } + ] + }, + "new_code_id": { + "description": "the code_id of the new logic to place in the given contract", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + } + } + }, + "additionalProperties": false + }, + { + "description": "Sets a new admin (for migrate) on the given contract. Fails if this contract is not currently admin of the target contract.", + "type": "object", + "required": [ + "update_admin" + ], + "properties": { + "update_admin": { + "type": "object", + "required": [ + "admin", + "contract_addr" + ], + "properties": { + "admin": { + "type": "string" + }, + "contract_addr": { + "type": "string" + } + } + } + }, + "additionalProperties": false + }, + { + "description": "Clears the admin on the given contract, so no more migration possible. Fails if this contract is not currently admin of the target contract.", + "type": "object", + "required": [ + "clear_admin" + ], + "properties": { + "clear_admin": { + "type": "object", + "required": [ + "contract_addr" + ], + "properties": { + "contract_addr": { + "type": "string" + } + } + } + }, + "additionalProperties": false + } + ] + } + } +} diff --git a/contracts/whitelist/src/contract.rs b/contracts/whitelist/src/contract.rs index a47b8d66..977c0340 100644 --- a/contracts/whitelist/src/contract.rs +++ b/contracts/whitelist/src/contract.rs @@ -162,7 +162,7 @@ mod tests { admins: vec![alice.to_string(), bob.to_string(), carl.to_string()], mutable: true, }; - let info = mock_info(&anyone, &[]); + let info = mock_info(anyone, &[]); instantiate(deps.as_mut(), mock_env(), info, instantiate_msg).unwrap(); // ensure expected config @@ -176,7 +176,7 @@ mod tests { let msg = ExecuteMsg::UpdateAdmins { admins: vec![anyone.to_string()], }; - let info = mock_info(&anyone, &[]); + let info = mock_info(anyone, &[]); let err = execute(deps.as_mut(), mock_env(), info, msg).unwrap_err(); assert_eq!(err, ContractError::Unauthorized {}); @@ -184,7 +184,7 @@ mod tests { let msg = ExecuteMsg::UpdateAdmins { admins: vec![alice.to_string(), bob.to_string()], }; - let info = mock_info(&alice, &[]); + let info = mock_info(alice, &[]); execute(deps.as_mut(), mock_env(), info, msg).unwrap(); // ensure expected config @@ -195,12 +195,12 @@ mod tests { assert_eq!(query_admin_list(deps.as_ref()).unwrap(), expected); // carl cannot freeze it - let info = mock_info(&carl, &[]); + let info = mock_info(carl, &[]); let err = execute(deps.as_mut(), mock_env(), info, ExecuteMsg::Freeze {}).unwrap_err(); assert_eq!(err, ContractError::Unauthorized {}); // but bob can - let info = mock_info(&bob, &[]); + let info = mock_info(bob, &[]); execute(deps.as_mut(), mock_env(), info, ExecuteMsg::Freeze {}).unwrap(); let expected = AdminListResponse { admins: vec![alice.to_string(), bob.to_string()], @@ -212,7 +212,7 @@ mod tests { let msg = ExecuteMsg::UpdateAdmins { admins: vec![alice.to_string()], }; - let info = mock_info(&alice, &[]); + let info = mock_info(alice, &[]); let err = execute(deps.as_mut(), mock_env(), info, msg).unwrap_err(); assert_eq!(err, ContractError::Unauthorized {}); } @@ -230,7 +230,7 @@ mod tests { admins: vec![alice.to_string(), carl.to_string()], mutable: false, }; - let info = mock_info(&bob, &[]); + let info = mock_info(bob, &[]); instantiate(deps.as_mut(), mock_env(), info, instantiate_msg).unwrap(); let freeze: ExecuteMsg = ExecuteMsg::Freeze {}; @@ -252,12 +252,12 @@ mod tests { let execute_msg = ExecuteMsg::Execute { msgs: msgs.clone() }; // bob cannot execute them - let info = mock_info(&bob, &[]); + let info = mock_info(bob, &[]); let err = execute(deps.as_mut(), mock_env(), info, execute_msg.clone()).unwrap_err(); assert_eq!(err, ContractError::Unauthorized {}); // but carl can - let info = mock_info(&carl, &[]); + let info = mock_info(carl, &[]); let res = execute(deps.as_mut(), mock_env(), info, execute_msg).unwrap(); assert_eq!( res.messages, @@ -280,7 +280,7 @@ mod tests { admins: vec![alice.to_string(), bob.to_string()], mutable: false, }; - let info = mock_info(&anyone, &[]); + let info = mock_info(anyone, &[]); instantiate(deps.as_mut(), mock_env(), info, instantiate_msg).unwrap(); // let us make some queries... different msg types by owner and by other diff --git a/packages/astroport/src/mock_querier.rs b/packages/astroport/src/mock_querier.rs index 37096dd1..3e176d14 100644 --- a/packages/astroport/src/mock_querier.rs +++ b/packages/astroport/src/mock_querier.rs @@ -63,10 +63,7 @@ pub(crate) fn balances_to_map( contract_balances_map.insert(addr.to_string(), **balance); } - balances_map.insert( - String::from(contract_addr.to_string()), - contract_balances_map, - ); + balances_map.insert(contract_addr.to_string(), contract_balances_map); } balances_map } @@ -149,7 +146,7 @@ impl CW20QueryHandler { pub fn execute(&self, request: &QueryRequest) -> QuerierResult { match &request { QueryRequest::Wasm(WasmQuery::Smart { contract_addr, msg }) => { - match from_binary(&msg).unwrap() { + match from_binary(msg).unwrap() { Cw20QueryMsg::TokenInfo {} => { let balances: &HashMap = match self.token_querier.balances.get(contract_addr) { @@ -170,7 +167,7 @@ impl CW20QueryHandler { name: "mAPPL".to_string(), symbol: "mAPPL".to_string(), decimals: 6, - total_supply: total_supply, + total_supply, }) .into(), ) @@ -238,7 +235,7 @@ impl DefaultQueryHandler { QueryRequest::Wasm(WasmQuery::Smart { contract_addr: _, msg, - }) => match from_binary(&msg).unwrap() { + }) => match from_binary(msg).unwrap() { FactoryQueryMsg::Pair { asset_infos } => { let key = asset_infos[0].to_string() + asset_infos[1].to_string().as_str(); match self.astroport_factory_querier.pairs.get(&key) { diff --git a/packages/astroport/src/testing.rs b/packages/astroport/src/testing.rs index 6959e407..3a9e96ca 100644 --- a/packages/astroport/src/testing.rs +++ b/packages/astroport/src/testing.rs @@ -109,24 +109,24 @@ fn test_asset_info() { denom: "uusd".to_string(), }; - assert_eq!(false, token_info.equal(&native_token_info)); + assert_ne!(token_info, native_token_info); - assert_eq!( - false, - token_info.equal(&AssetInfo::Token { + assert_ne!( + token_info, + AssetInfo::Token { contract_addr: Addr::unchecked("asset0001"), - }) + } ); assert_eq!( - true, - token_info.equal(&AssetInfo::Token { + token_info, + AssetInfo::Token { contract_addr: Addr::unchecked("asset0000"), - }) + } ); - assert_eq!(true, native_token_info.is_native_token()); - assert_eq!(false, token_info.is_native_token()); + assert!(native_token_info.is_native_token()); + assert!(!token_info.is_native_token()); let mut deps = mock_dependencies(&[Coin { denom: "uusd".to_string(), diff --git a/packages/cw721/.cargo/config b/packages/cw721/.cargo/config new file mode 100644 index 00000000..b613a59f --- /dev/null +++ b/packages/cw721/.cargo/config @@ -0,0 +1,4 @@ +[alias] +wasm = "build --release --target wasm32-unknown-unknown" +wasm-debug = "build --target wasm32-unknown-unknown" +schema = "run --example schema" diff --git a/packages/cw721/Cargo.toml b/packages/cw721/Cargo.toml new file mode 100644 index 00000000..d092d45b --- /dev/null +++ b/packages/cw721/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "cw721" +version = "0.10.1" +authors = ["Ethan Frey "] +edition = "2018" +description = "Definition and types for the CosmWasm-721 NFT interface" +license = "Apache-2.0" +repository = "https://github.com/CosmWasm/cw-nfts" +homepage = "https://cosmwasm.com" +documentation = "https://docs.cosmwasm.com" + +[dependencies] +cw0 = "0.10.3" +cosmwasm-std = "1.0.0" +schemars = "0.8.10" +serde = { version = "1.0.137", default-features = false, features = ["derive"] } + +[dev-dependencies] +cosmwasm-schema = "1.0.0" diff --git a/packages/cw721/NOTICE b/packages/cw721/NOTICE new file mode 100644 index 00000000..c956affa --- /dev/null +++ b/packages/cw721/NOTICE @@ -0,0 +1,14 @@ +CW721: A CosmWasm spec for non-fungible token contracts +Copyright (C) 2020-2021 Confio OÜ + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/packages/cw721/README.md b/packages/cw721/README.md new file mode 100644 index 00000000..6d065ae8 --- /dev/null +++ b/packages/cw721/README.md @@ -0,0 +1,122 @@ +# CW721 Spec: Non Fungible Tokens + +CW721 is a specification for non-fungible tokens based on CosmWasm. +The name and design is based on Ethereum's ERC721 standard, +with some enhancements. The types in here can be imported by +contracts that wish to implement this spec, or by contracts that call +to any standard cw721 contract. + +The specification is split into multiple sections, a contract may only +implement some of this functionality, but must implement the base. + +## Base + +This handles ownership, transfers, and allowances. These must be supported +as is by all CW721 contracts. Note that all tokens must have an owner, +as well as an ID. The ID is an arbitrary string, unique within the contract. + +### Messages + +`TransferNft{recipient, token_id}` - +This transfers ownership of the token to `recipient` account. This is +designed to send to an address controlled by a private key and *does not* +trigger any actions on the recipient if it is a contract. + +Requires `token_id` to point to a valid token, and `env.sender` to be +the owner of it, or have an allowance to transfer it. + +`SendNft{contract, token_id, msg}` - +This transfers ownership of the token to `contract` account. `contract` +must be an address controlled by a smart contract, which implements +the CW721Receiver interface. The `msg` will be passed to the recipient +contract, along with the token_id. + +Requires `token_id` to point to a valid token, and `env.sender` to be +the owner of it, or have an allowance to transfer it. + +`Approve{spender, token_id, expires}` - Grants permission to `spender` to +transfer or send the given token. This can only be performed when +`env.sender` is the owner of the given `token_id` or an `operator`. +There can multiple spender accounts per token, and they are cleared once +the token is transfered or sent. + +`Revoke{spender, token_id}` - This revokes a previously granted permission +to transfer the given `token_id`. This can only be granted when +`env.sender` is the owner of the given `token_id` or an `operator`. + +`ApproveAll{operator, expires}` - Grant `operator` permission to transfer or send +all tokens owned by `env.sender`. This approval is tied to the owner, not the +tokens and applies to any future token that the owner receives as well. + +`RevokeAll{operator}` - Revoke a previous `ApproveAll` permission granted +to the given `operator`. + +### Queries + +`OwnerOf{token_id}` - Returns the owner of the given token, +as well as anyone with approval on this particular token. +If the token is unknown, returns an error. Return type is +`OwnerResponse{owner}`. + +`ApprovedForAll{owner, include_expired}` - List all operators that can +access all of the owner's tokens. Return type is `ApprovedForAllResponse`. +If `include_expired` is set, show expired owners in the results, otherwise, +ignore them. + +`NumTokens{}` - Total number of tokens issued + +### Receiver + +The counter-part to `SendNft` is `ReceiveNft`, which must be implemented by +any contract that wishes to manage CW721 tokens. This is generally *not* +implemented by any CW721 contract. + +`ReceiveNft{sender, token_id, msg}` - This is designed to handle `SendNft` +messages. The address of the contract is stored in `env.sender` +so it cannot be faked. The contract should ensure the sender matches +the token contract it expects to handle, and not allow arbitrary addresses. + +The `sender` is the original account requesting to move the token +and `msg` is a `Binary` data that can be decoded into a contract-specific +message. This can be empty if we have only one default action, +or it may be a `ReceiveMsg` variant to clarify the intention. For example, +if I send to an exchange, I can specify the price I want to list the token +for. + +## Metadata + +### Queries + +`ContractInfo{}` - This returns top-level metadata about the contract. +Namely, `name` and `symbol`. + +`NftInfo{token_id}` - This returns metadata about one particular token. +The return value is based on *ERC721 Metadata JSON Schema*, but directly +from the contract, not as a Uri. Only the image link is a Uri. + +`AllNftInfo{token_id}` - This returns the result of both `NftInfo` +and `OwnerOf` as one query as an optimization for clients, which may +want both info to display one NFT. + +## Enumerable + +### Queries + +Pagination is acheived via `start_after` and `limit`. Limit is a request +set by the client, if unset, the contract will automatically set it to +`DefaultLimit` (suggested 10). If set, it will be used up to a `MaxLimit` +value (suggested 30). Contracts can define other `DefaultLimit` and `MaxLimit` +values without violating the CW721 spec, and clients should not rely on +any particular values. + +If `start_after` is unset, the query returns the first results, ordered by +lexogaphically by `token_id`. If `start_after` is set, then it returns the +first `limit` tokens *after* the given one. This allows straight-forward +pagination by taking the last result returned (a `token_id`) and using it +as the `start_after` value in a future query. + +`Tokens{owner, start_after, limit}` - List all token_ids that belong to a given owner. +Return type is `TokensResponse{tokens: Vec}`. + +`AllTokens{start_after, limit}` - Requires pagination. Lists all token_ids controlled by +the contract. diff --git a/packages/cw721/examples/cw721_schema.rs b/packages/cw721/examples/cw721_schema.rs new file mode 100644 index 00000000..c0c47e83 --- /dev/null +++ b/packages/cw721/examples/cw721_schema.rs @@ -0,0 +1,41 @@ +use std::env::current_dir; +use std::fs::create_dir_all; + +use cosmwasm_schema::{export_schema, export_schema_with_title, remove_schemas, schema_for}; +use cosmwasm_std::Empty; + +use cw721::{ + AllNftInfoResponse, ApprovalResponse, ApprovalsResponse, ContractInfoResponse, Cw721ExecuteMsg, + Cw721QueryMsg, Cw721ReceiveMsg, NftInfoResponse, NumTokensResponse, OperatorsResponse, + OwnerOfResponse, TokensResponse, +}; + +type Extension = Option; + +fn main() { + let mut out_dir = current_dir().unwrap(); + out_dir.push("schema"); + create_dir_all(&out_dir).unwrap(); + remove_schemas(&out_dir).unwrap(); + + export_schema(&schema_for!(Cw721ExecuteMsg), &out_dir); + export_schema(&schema_for!(Cw721QueryMsg), &out_dir); + export_schema(&schema_for!(Cw721ReceiveMsg), &out_dir); + export_schema_with_title( + &schema_for!(AllNftInfoResponse), + &out_dir, + "AllNftInfoResponse", + ); + export_schema(&schema_for!(ApprovalResponse), &out_dir); + export_schema(&schema_for!(ApprovalsResponse), &out_dir); + export_schema(&schema_for!(OperatorsResponse), &out_dir); + export_schema(&schema_for!(ContractInfoResponse), &out_dir); + export_schema(&schema_for!(OwnerOfResponse), &out_dir); + export_schema_with_title( + &schema_for!(NftInfoResponse), + &out_dir, + "NftInfoResponse", + ); + export_schema(&schema_for!(NumTokensResponse), &out_dir); + export_schema(&schema_for!(TokensResponse), &out_dir); +} diff --git a/packages/cw721/schema/all_nft_info_response.json b/packages/cw721/schema/all_nft_info_response.json new file mode 100644 index 00000000..bfc334bf --- /dev/null +++ b/packages/cw721/schema/all_nft_info_response.json @@ -0,0 +1,155 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "AllNftInfoResponse", + "type": "object", + "required": [ + "access", + "info" + ], + "properties": { + "access": { + "description": "Who can transfer the token", + "allOf": [ + { + "$ref": "#/definitions/OwnerOfResponse" + } + ] + }, + "info": { + "description": "Data on the token itself,", + "allOf": [ + { + "$ref": "#/definitions/NftInfoResponse_for_Nullable_Empty" + } + ] + } + }, + "definitions": { + "Approval": { + "type": "object", + "required": [ + "expires", + "spender" + ], + "properties": { + "expires": { + "description": "When the Approval expires (maybe Expiration::never)", + "allOf": [ + { + "$ref": "#/definitions/Expiration" + } + ] + }, + "spender": { + "description": "Account that can transfer/send the token", + "type": "string" + } + } + }, + "Empty": { + "description": "An empty struct that serves as a placeholder in different places, such as contracts that don't set a custom message.\n\nIt is designed to be expressable in correct JSON and JSON Schema but contains no meaningful data. Previously we used enums without cases, but those cannot represented as valid JSON Schema (https://github.com/CosmWasm/cosmwasm/issues/451)", + "type": "object" + }, + "Expiration": { + "description": "Expiration represents a point in time when some event happens. It can compare with a BlockInfo and will return is_expired() == true once the condition is hit (and for every block in the future)", + "oneOf": [ + { + "description": "AtHeight will expire when `env.block.height` >= height", + "type": "object", + "required": [ + "at_height" + ], + "properties": { + "at_height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + { + "description": "AtTime will expire when `env.block.time` >= time", + "type": "object", + "required": [ + "at_time" + ], + "properties": { + "at_time": { + "$ref": "#/definitions/Timestamp" + } + }, + "additionalProperties": false + }, + { + "description": "Never will never expire. Used to express the empty variant", + "type": "object", + "required": [ + "never" + ], + "properties": { + "never": { + "type": "object" + } + }, + "additionalProperties": false + } + ] + }, + "NftInfoResponse_for_Nullable_Empty": { + "type": "object", + "properties": { + "extension": { + "description": "You can add any custom metadata here when you extend cw721-base", + "anyOf": [ + { + "$ref": "#/definitions/Empty" + }, + { + "type": "null" + } + ] + }, + "token_uri": { + "description": "Universal resource identifier for this NFT Should point to a JSON file that conforms to the ERC721 Metadata JSON Schema", + "type": [ + "string", + "null" + ] + } + } + }, + "OwnerOfResponse": { + "type": "object", + "required": [ + "approvals", + "owner" + ], + "properties": { + "approvals": { + "description": "If set this address is approved to transfer/send the token as well", + "type": "array", + "items": { + "$ref": "#/definitions/Approval" + } + }, + "owner": { + "description": "Owner of the token", + "type": "string" + } + } + }, + "Timestamp": { + "description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```", + "allOf": [ + { + "$ref": "#/definitions/Uint64" + } + ] + }, + "Uint64": { + "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```", + "type": "string" + } + } +} diff --git a/packages/cw721/schema/approval_response.json b/packages/cw721/schema/approval_response.json new file mode 100644 index 00000000..4f45b42e --- /dev/null +++ b/packages/cw721/schema/approval_response.json @@ -0,0 +1,94 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ApprovalResponse", + "type": "object", + "required": [ + "approval" + ], + "properties": { + "approval": { + "$ref": "#/definitions/Approval" + } + }, + "definitions": { + "Approval": { + "type": "object", + "required": [ + "expires", + "spender" + ], + "properties": { + "expires": { + "description": "When the Approval expires (maybe Expiration::never)", + "allOf": [ + { + "$ref": "#/definitions/Expiration" + } + ] + }, + "spender": { + "description": "Account that can transfer/send the token", + "type": "string" + } + } + }, + "Expiration": { + "description": "Expiration represents a point in time when some event happens. It can compare with a BlockInfo and will return is_expired() == true once the condition is hit (and for every block in the future)", + "oneOf": [ + { + "description": "AtHeight will expire when `env.block.height` >= height", + "type": "object", + "required": [ + "at_height" + ], + "properties": { + "at_height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + { + "description": "AtTime will expire when `env.block.time` >= time", + "type": "object", + "required": [ + "at_time" + ], + "properties": { + "at_time": { + "$ref": "#/definitions/Timestamp" + } + }, + "additionalProperties": false + }, + { + "description": "Never will never expire. Used to express the empty variant", + "type": "object", + "required": [ + "never" + ], + "properties": { + "never": { + "type": "object" + } + }, + "additionalProperties": false + } + ] + }, + "Timestamp": { + "description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```", + "allOf": [ + { + "$ref": "#/definitions/Uint64" + } + ] + }, + "Uint64": { + "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```", + "type": "string" + } + } +} diff --git a/packages/cw721/schema/approvals_response.json b/packages/cw721/schema/approvals_response.json new file mode 100644 index 00000000..8d8e39ea --- /dev/null +++ b/packages/cw721/schema/approvals_response.json @@ -0,0 +1,97 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ApprovalsResponse", + "type": "object", + "required": [ + "approvals" + ], + "properties": { + "approvals": { + "type": "array", + "items": { + "$ref": "#/definitions/Approval" + } + } + }, + "definitions": { + "Approval": { + "type": "object", + "required": [ + "expires", + "spender" + ], + "properties": { + "expires": { + "description": "When the Approval expires (maybe Expiration::never)", + "allOf": [ + { + "$ref": "#/definitions/Expiration" + } + ] + }, + "spender": { + "description": "Account that can transfer/send the token", + "type": "string" + } + } + }, + "Expiration": { + "description": "Expiration represents a point in time when some event happens. It can compare with a BlockInfo and will return is_expired() == true once the condition is hit (and for every block in the future)", + "oneOf": [ + { + "description": "AtHeight will expire when `env.block.height` >= height", + "type": "object", + "required": [ + "at_height" + ], + "properties": { + "at_height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + { + "description": "AtTime will expire when `env.block.time` >= time", + "type": "object", + "required": [ + "at_time" + ], + "properties": { + "at_time": { + "$ref": "#/definitions/Timestamp" + } + }, + "additionalProperties": false + }, + { + "description": "Never will never expire. Used to express the empty variant", + "type": "object", + "required": [ + "never" + ], + "properties": { + "never": { + "type": "object" + } + }, + "additionalProperties": false + } + ] + }, + "Timestamp": { + "description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```", + "allOf": [ + { + "$ref": "#/definitions/Uint64" + } + ] + }, + "Uint64": { + "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```", + "type": "string" + } + } +} diff --git a/packages/cw721/schema/contract_info_response.json b/packages/cw721/schema/contract_info_response.json new file mode 100644 index 00000000..a1671258 --- /dev/null +++ b/packages/cw721/schema/contract_info_response.json @@ -0,0 +1,17 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ContractInfoResponse", + "type": "object", + "required": [ + "name", + "symbol" + ], + "properties": { + "name": { + "type": "string" + }, + "symbol": { + "type": "string" + } + } +} diff --git a/packages/cw721/schema/cw721_execute_msg.json b/packages/cw721/schema/cw721_execute_msg.json new file mode 100644 index 00000000..ccb3fa8c --- /dev/null +++ b/packages/cw721/schema/cw721_execute_msg.json @@ -0,0 +1,236 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Cw721ExecuteMsg", + "oneOf": [ + { + "description": "Transfer is a base message to move a token to another account without triggering actions", + "type": "object", + "required": [ + "transfer_nft" + ], + "properties": { + "transfer_nft": { + "type": "object", + "required": [ + "recipient", + "token_id" + ], + "properties": { + "recipient": { + "type": "string" + }, + "token_id": { + "type": "string" + } + } + } + }, + "additionalProperties": false + }, + { + "description": "Send is a base message to transfer a token to a contract and trigger an action on the receiving contract.", + "type": "object", + "required": [ + "send_nft" + ], + "properties": { + "send_nft": { + "type": "object", + "required": [ + "contract", + "msg", + "token_id" + ], + "properties": { + "contract": { + "type": "string" + }, + "msg": { + "$ref": "#/definitions/Binary" + }, + "token_id": { + "type": "string" + } + } + } + }, + "additionalProperties": false + }, + { + "description": "Allows operator to transfer / send the token from the owner's account. If expiration is set, then this allowance has a time/height limit", + "type": "object", + "required": [ + "approve" + ], + "properties": { + "approve": { + "type": "object", + "required": [ + "spender", + "token_id" + ], + "properties": { + "expires": { + "anyOf": [ + { + "$ref": "#/definitions/Expiration" + }, + { + "type": "null" + } + ] + }, + "spender": { + "type": "string" + }, + "token_id": { + "type": "string" + } + } + } + }, + "additionalProperties": false + }, + { + "description": "Remove previously granted Approval", + "type": "object", + "required": [ + "revoke" + ], + "properties": { + "revoke": { + "type": "object", + "required": [ + "spender", + "token_id" + ], + "properties": { + "spender": { + "type": "string" + }, + "token_id": { + "type": "string" + } + } + } + }, + "additionalProperties": false + }, + { + "description": "Allows operator to transfer / send any token from the owner's account. If expiration is set, then this allowance has a time/height limit", + "type": "object", + "required": [ + "approve_all" + ], + "properties": { + "approve_all": { + "type": "object", + "required": [ + "operator" + ], + "properties": { + "expires": { + "anyOf": [ + { + "$ref": "#/definitions/Expiration" + }, + { + "type": "null" + } + ] + }, + "operator": { + "type": "string" + } + } + } + }, + "additionalProperties": false + }, + { + "description": "Remove previously granted ApproveAll permission", + "type": "object", + "required": [ + "revoke_all" + ], + "properties": { + "revoke_all": { + "type": "object", + "required": [ + "operator" + ], + "properties": { + "operator": { + "type": "string" + } + } + } + }, + "additionalProperties": false + } + ], + "definitions": { + "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", + "type": "string" + }, + "Expiration": { + "description": "Expiration represents a point in time when some event happens. It can compare with a BlockInfo and will return is_expired() == true once the condition is hit (and for every block in the future)", + "oneOf": [ + { + "description": "AtHeight will expire when `env.block.height` >= height", + "type": "object", + "required": [ + "at_height" + ], + "properties": { + "at_height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + { + "description": "AtTime will expire when `env.block.time` >= time", + "type": "object", + "required": [ + "at_time" + ], + "properties": { + "at_time": { + "$ref": "#/definitions/Timestamp" + } + }, + "additionalProperties": false + }, + { + "description": "Never will never expire. Used to express the empty variant", + "type": "object", + "required": [ + "never" + ], + "properties": { + "never": { + "type": "object" + } + }, + "additionalProperties": false + } + ] + }, + "Timestamp": { + "description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```", + "allOf": [ + { + "$ref": "#/definitions/Uint64" + } + ] + }, + "Uint64": { + "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```", + "type": "string" + } + } +} diff --git a/packages/cw721/schema/cw721_query_msg.json b/packages/cw721/schema/cw721_query_msg.json new file mode 100644 index 00000000..383e1b43 --- /dev/null +++ b/packages/cw721/schema/cw721_query_msg.json @@ -0,0 +1,240 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Cw721QueryMsg", + "oneOf": [ + { + "description": "Return the owner of the given token, error if token does not exist Return type: OwnerOfResponse", + "type": "object", + "required": [ + "owner_of" + ], + "properties": { + "owner_of": { + "type": "object", + "required": [ + "token_id" + ], + "properties": { + "include_expired": { + "description": "unset or false will filter out expired approvals, you must set to true to see them", + "type": [ + "boolean", + "null" + ] + }, + "token_id": { + "type": "string" + } + } + } + }, + "additionalProperties": false + }, + { + "description": "Return operator that can access all of the owner's tokens. Return type: `ApprovedResponse`", + "type": "object", + "required": [ + "approved" + ], + "properties": { + "approved": { + "type": "object", + "required": [ + "operator", + "owner" + ], + "properties": { + "operator": { + "type": "string" + }, + "owner": { + "type": "string" + } + } + } + }, + "additionalProperties": false + }, + { + "description": "List all operators that can access all of the owner's tokens. Return type: `ApprovedForAllResponse`", + "type": "object", + "required": [ + "approved_for_all" + ], + "properties": { + "approved_for_all": { + "type": "object", + "required": [ + "owner" + ], + "properties": { + "include_expired": { + "description": "unset or false will filter out expired approvals, you must set to true to see them", + "type": [ + "boolean", + "null" + ] + }, + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "owner": { + "type": "string" + }, + "start_after": { + "type": [ + "string", + "null" + ] + } + } + } + }, + "additionalProperties": false + }, + { + "description": "Total number of tokens issued", + "type": "object", + "required": [ + "num_tokens" + ], + "properties": { + "num_tokens": { + "type": "object" + } + }, + "additionalProperties": false + }, + { + "description": "With MetaData Extension. Returns top-level metadata about the contract: `ContractInfoResponse`", + "type": "object", + "required": [ + "contract_info" + ], + "properties": { + "contract_info": { + "type": "object" + } + }, + "additionalProperties": false + }, + { + "description": "With MetaData Extension. Returns metadata about one particular token, based on *ERC721 Metadata JSON Schema* but directly from the contract: `NftInfoResponse`", + "type": "object", + "required": [ + "nft_info" + ], + "properties": { + "nft_info": { + "type": "object", + "required": [ + "token_id" + ], + "properties": { + "token_id": { + "type": "string" + } + } + } + }, + "additionalProperties": false + }, + { + "description": "With MetaData Extension. Returns the result of both `NftInfo` and `OwnerOf` as one query as an optimization for clients: `AllNftInfo`", + "type": "object", + "required": [ + "all_nft_info" + ], + "properties": { + "all_nft_info": { + "type": "object", + "required": [ + "token_id" + ], + "properties": { + "include_expired": { + "description": "unset or false will filter out expired approvals, you must set to true to see them", + "type": [ + "boolean", + "null" + ] + }, + "token_id": { + "type": "string" + } + } + } + }, + "additionalProperties": false + }, + { + "description": "With Enumerable extension. Returns all tokens owned by the given address, [] if unset. Return type: TokensResponse.", + "type": "object", + "required": [ + "tokens" + ], + "properties": { + "tokens": { + "type": "object", + "required": [ + "owner" + ], + "properties": { + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "owner": { + "type": "string" + }, + "start_after": { + "type": [ + "string", + "null" + ] + } + } + } + }, + "additionalProperties": false + }, + { + "description": "With Enumerable extension. Requires pagination. Lists all token_ids controlled by the contract. Return type: TokensResponse.", + "type": "object", + "required": [ + "all_tokens" + ], + "properties": { + "all_tokens": { + "type": "object", + "properties": { + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "type": [ + "string", + "null" + ] + } + } + } + }, + "additionalProperties": false + } + ] +} diff --git a/packages/cw721/schema/cw721_receive_msg.json b/packages/cw721/schema/cw721_receive_msg.json new file mode 100644 index 00000000..36f0a3a3 --- /dev/null +++ b/packages/cw721/schema/cw721_receive_msg.json @@ -0,0 +1,28 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Cw721ReceiveMsg", + "description": "Cw721ReceiveMsg should be de/serialized under `Receive()` variant in a ExecuteMsg", + "type": "object", + "required": [ + "msg", + "sender", + "token_id" + ], + "properties": { + "msg": { + "$ref": "#/definitions/Binary" + }, + "sender": { + "type": "string" + }, + "token_id": { + "type": "string" + } + }, + "definitions": { + "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", + "type": "string" + } + } +} diff --git a/packages/cw721/schema/nft_info_response.json b/packages/cw721/schema/nft_info_response.json new file mode 100644 index 00000000..e6bf1d40 --- /dev/null +++ b/packages/cw721/schema/nft_info_response.json @@ -0,0 +1,31 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "NftInfoResponse", + "type": "object", + "properties": { + "extension": { + "description": "You can add any custom metadata here when you extend cw721-base", + "anyOf": [ + { + "$ref": "#/definitions/Empty" + }, + { + "type": "null" + } + ] + }, + "token_uri": { + "description": "Universal resource identifier for this NFT Should point to a JSON file that conforms to the ERC721 Metadata JSON Schema", + "type": [ + "string", + "null" + ] + } + }, + "definitions": { + "Empty": { + "description": "An empty struct that serves as a placeholder in different places, such as contracts that don't set a custom message.\n\nIt is designed to be expressable in correct JSON and JSON Schema but contains no meaningful data. Previously we used enums without cases, but those cannot represented as valid JSON Schema (https://github.com/CosmWasm/cosmwasm/issues/451)", + "type": "object" + } + } +} diff --git a/packages/cw721/schema/num_tokens_response.json b/packages/cw721/schema/num_tokens_response.json new file mode 100644 index 00000000..4647c23a --- /dev/null +++ b/packages/cw721/schema/num_tokens_response.json @@ -0,0 +1,15 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "NumTokensResponse", + "type": "object", + "required": [ + "count" + ], + "properties": { + "count": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + } +} diff --git a/packages/cw721/schema/operators_response.json b/packages/cw721/schema/operators_response.json new file mode 100644 index 00000000..53703072 --- /dev/null +++ b/packages/cw721/schema/operators_response.json @@ -0,0 +1,97 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "OperatorsResponse", + "type": "object", + "required": [ + "operators" + ], + "properties": { + "operators": { + "type": "array", + "items": { + "$ref": "#/definitions/Approval" + } + } + }, + "definitions": { + "Approval": { + "type": "object", + "required": [ + "expires", + "spender" + ], + "properties": { + "expires": { + "description": "When the Approval expires (maybe Expiration::never)", + "allOf": [ + { + "$ref": "#/definitions/Expiration" + } + ] + }, + "spender": { + "description": "Account that can transfer/send the token", + "type": "string" + } + } + }, + "Expiration": { + "description": "Expiration represents a point in time when some event happens. It can compare with a BlockInfo and will return is_expired() == true once the condition is hit (and for every block in the future)", + "oneOf": [ + { + "description": "AtHeight will expire when `env.block.height` >= height", + "type": "object", + "required": [ + "at_height" + ], + "properties": { + "at_height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + { + "description": "AtTime will expire when `env.block.time` >= time", + "type": "object", + "required": [ + "at_time" + ], + "properties": { + "at_time": { + "$ref": "#/definitions/Timestamp" + } + }, + "additionalProperties": false + }, + { + "description": "Never will never expire. Used to express the empty variant", + "type": "object", + "required": [ + "never" + ], + "properties": { + "never": { + "type": "object" + } + }, + "additionalProperties": false + } + ] + }, + "Timestamp": { + "description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```", + "allOf": [ + { + "$ref": "#/definitions/Uint64" + } + ] + }, + "Uint64": { + "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```", + "type": "string" + } + } +} diff --git a/packages/cw721/schema/owner_of_response.json b/packages/cw721/schema/owner_of_response.json new file mode 100644 index 00000000..1258d671 --- /dev/null +++ b/packages/cw721/schema/owner_of_response.json @@ -0,0 +1,103 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "OwnerOfResponse", + "type": "object", + "required": [ + "approvals", + "owner" + ], + "properties": { + "approvals": { + "description": "If set this address is approved to transfer/send the token as well", + "type": "array", + "items": { + "$ref": "#/definitions/Approval" + } + }, + "owner": { + "description": "Owner of the token", + "type": "string" + } + }, + "definitions": { + "Approval": { + "type": "object", + "required": [ + "expires", + "spender" + ], + "properties": { + "expires": { + "description": "When the Approval expires (maybe Expiration::never)", + "allOf": [ + { + "$ref": "#/definitions/Expiration" + } + ] + }, + "spender": { + "description": "Account that can transfer/send the token", + "type": "string" + } + } + }, + "Expiration": { + "description": "Expiration represents a point in time when some event happens. It can compare with a BlockInfo and will return is_expired() == true once the condition is hit (and for every block in the future)", + "oneOf": [ + { + "description": "AtHeight will expire when `env.block.height` >= height", + "type": "object", + "required": [ + "at_height" + ], + "properties": { + "at_height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + { + "description": "AtTime will expire when `env.block.time` >= time", + "type": "object", + "required": [ + "at_time" + ], + "properties": { + "at_time": { + "$ref": "#/definitions/Timestamp" + } + }, + "additionalProperties": false + }, + { + "description": "Never will never expire. Used to express the empty variant", + "type": "object", + "required": [ + "never" + ], + "properties": { + "never": { + "type": "object" + } + }, + "additionalProperties": false + } + ] + }, + "Timestamp": { + "description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```", + "allOf": [ + { + "$ref": "#/definitions/Uint64" + } + ] + }, + "Uint64": { + "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```", + "type": "string" + } + } +} diff --git a/packages/cw721/schema/tokens_response.json b/packages/cw721/schema/tokens_response.json new file mode 100644 index 00000000..b8e3d75b --- /dev/null +++ b/packages/cw721/schema/tokens_response.json @@ -0,0 +1,17 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "TokensResponse", + "type": "object", + "required": [ + "tokens" + ], + "properties": { + "tokens": { + "description": "Contains all token_ids in lexicographical ordering If there are more than `limit`, use `start_from` in future queries to achieve pagination.", + "type": "array", + "items": { + "type": "string" + } + } + } +} diff --git a/packages/cw721/src/lib.rs b/packages/cw721/src/lib.rs new file mode 100644 index 00000000..b1ac94ef --- /dev/null +++ b/packages/cw721/src/lib.rs @@ -0,0 +1,15 @@ +mod msg; +mod query; +mod receiver; +mod traits; + +pub use cw0::Expiration; + +pub use crate::msg::Cw721ExecuteMsg; +pub use crate::query::{ + AllNftInfoResponse, Approval, ApprovalResponse, ApprovalsResponse, ContractInfoResponse, + Cw721QueryMsg, NftInfoResponse, NumTokensResponse, OperatorsResponse, OwnerOfResponse, + TokensResponse, +}; +pub use crate::receiver::Cw721ReceiveMsg; +pub use crate::traits::{CustomMsg, Cw721, Cw721Execute, Cw721Query}; diff --git a/packages/cw721/src/msg.rs b/packages/cw721/src/msg.rs new file mode 100644 index 00000000..67c0ff6c --- /dev/null +++ b/packages/cw721/src/msg.rs @@ -0,0 +1,36 @@ +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use cosmwasm_std::Binary; +use cw0::Expiration; + +#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] +#[serde(rename_all = "snake_case")] +pub enum Cw721ExecuteMsg { + /// Transfer is a base message to move a token to another account without triggering actions + TransferNft { recipient: String, token_id: String }, + /// Send is a base message to transfer a token to a contract and trigger an action + /// on the receiving contract. + SendNft { + contract: String, + token_id: String, + msg: Binary, + }, + /// Allows operator to transfer / send the token from the owner's account. + /// If expiration is set, then this allowance has a time/height limit + Approve { + spender: String, + token_id: String, + expires: Option, + }, + /// Remove previously granted Approval + Revoke { spender: String, token_id: String }, + /// Allows operator to transfer / send any token from the owner's account. + /// If expiration is set, then this allowance has a time/height limit + ApproveAll { + operator: String, + expires: Option, + }, + /// Remove previously granted ApproveAll permission + RevokeAll { operator: String }, +} diff --git a/packages/cw721/src/query.rs b/packages/cw721/src/query.rs new file mode 100644 index 00000000..aada541a --- /dev/null +++ b/packages/cw721/src/query.rs @@ -0,0 +1,132 @@ +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use cw0::Expiration; + +#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] +#[serde(rename_all = "snake_case")] +pub enum Cw721QueryMsg { + /// Return the owner of the given token, error if token does not exist + /// Return type: OwnerOfResponse + OwnerOf { + token_id: String, + /// unset or false will filter out expired approvals, you must set to true to see them + include_expired: Option, + }, + + /// Return operator that can access all of the owner's tokens. + /// Return type: `ApprovedResponse` + Approved { owner: String, operator: String }, + + /// List all operators that can access all of the owner's tokens. + /// Return type: `ApprovedForAllResponse` + ApprovedForAll { + owner: String, + /// unset or false will filter out expired approvals, you must set to true to see them + include_expired: Option, + start_after: Option, + limit: Option, + }, + /// Total number of tokens issued + NumTokens {}, + + /// With MetaData Extension. + /// Returns top-level metadata about the contract: `ContractInfoResponse` + ContractInfo {}, + /// With MetaData Extension. + /// Returns metadata about one particular token, based on *ERC721 Metadata JSON Schema* + /// but directly from the contract: `NftInfoResponse` + NftInfo { token_id: String }, + /// With MetaData Extension. + /// Returns the result of both `NftInfo` and `OwnerOf` as one query as an optimization + /// for clients: `AllNftInfo` + AllNftInfo { + token_id: String, + /// unset or false will filter out expired approvals, you must set to true to see them + include_expired: Option, + }, + + /// With Enumerable extension. + /// Returns all tokens owned by the given address, [] if unset. + /// Return type: TokensResponse. + Tokens { + owner: String, + start_after: Option, + limit: Option, + }, + /// With Enumerable extension. + /// Requires pagination. Lists all token_ids controlled by the contract. + /// Return type: TokensResponse. + AllTokens { + start_after: Option, + limit: Option, + }, +} + +#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] +pub struct OwnerOfResponse { + /// Owner of the token + pub owner: String, + /// If set this address is approved to transfer/send the token as well + pub approvals: Vec, +} + +#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] +pub struct Approval { + /// Account that can transfer/send the token + pub spender: String, + /// When the Approval expires (maybe Expiration::never) + pub expires: Expiration, +} + +#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] +pub struct ApprovalResponse { + pub approval: Approval, +} + +#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] +pub struct ApprovalsResponse { + pub approvals: Vec, +} + +#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] +pub struct OperatorsResponse { + pub operators: Vec, +} + +#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] +pub struct NumTokensResponse { + pub count: u64, +} + +#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] +pub struct ContractInfoResponse { + pub name: String, + pub symbol: String, +} + +#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] +pub struct NftInfoResponse { + /// Universal resource identifier for this NFT + /// Should point to a JSON file that conforms to the ERC721 + /// Metadata JSON Schema + pub token_uri: Option, + /// You can add any custom metadata here when you extend cw721-base + pub extension: T, +} + +#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] +pub struct AllNftInfoResponse { + /// Who can transfer the token + pub access: OwnerOfResponse, + /// Data on the token itself, + pub info: NftInfoResponse, +} + +#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] +pub struct TokensResponse { + /// Contains all token_ids in lexicographical ordering + /// If there are more than `limit`, use `start_from` in future queries + /// to achieve pagination. + pub tokens: Vec, +} diff --git a/packages/cw721/src/receiver.rs b/packages/cw721/src/receiver.rs new file mode 100644 index 00000000..e16f66ae --- /dev/null +++ b/packages/cw721/src/receiver.rs @@ -0,0 +1,43 @@ +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use cosmwasm_std::{to_binary, Binary, CosmosMsg, StdResult, WasmMsg}; + +/// Cw721ReceiveMsg should be de/serialized under `Receive()` variant in a ExecuteMsg +#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] +#[serde(rename_all = "snake_case")] +pub struct Cw721ReceiveMsg { + pub sender: String, + pub token_id: String, + pub msg: Binary, +} + +impl Cw721ReceiveMsg { + /// serializes the message + pub fn into_binary(self) -> StdResult { + let msg = ReceiverExecuteMsg::ReceiveNft(self); + to_binary(&msg) + } + + /// creates a cosmos_msg sending this struct to the named contract + pub fn into_cosmos_msg, C>(self, contract_addr: T) -> StdResult> + where + C: Clone + std::fmt::Debug + PartialEq + JsonSchema, + { + let msg = self.into_binary()?; + let execute = WasmMsg::Execute { + contract_addr: contract_addr.into(), + msg, + funds: vec![], + }; + Ok(execute.into()) + } +} + +/// This is just a helper to properly serialize the above message. +/// The actual receiver should include this variant in the larger ExecuteMsg enum +#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] +#[serde(rename_all = "snake_case")] +enum ReceiverExecuteMsg { + ReceiveNft(Cw721ReceiveMsg), +} diff --git a/packages/cw721/src/traits.rs b/packages/cw721/src/traits.rs new file mode 100644 index 00000000..eb36aa79 --- /dev/null +++ b/packages/cw721/src/traits.rs @@ -0,0 +1,166 @@ +use schemars::JsonSchema; +use serde::de::DeserializeOwned; +use serde::Serialize; + +use crate::query::ApprovalResponse; +use crate::{ + AllNftInfoResponse, ApprovalsResponse, ContractInfoResponse, NftInfoResponse, + NumTokensResponse, OperatorsResponse, OwnerOfResponse, TokensResponse, +}; +use cosmwasm_std::{Binary, Deps, DepsMut, Empty, Env, MessageInfo, Response, StdResult}; +use cw0::Expiration; + +// TODO: move this somewhere else... ideally cosmwasm-std +pub trait CustomMsg: Clone + std::fmt::Debug + PartialEq + JsonSchema {} + +impl CustomMsg for Empty {} + +pub trait Cw721: Cw721Execute + Cw721Query +where + T: Serialize + DeserializeOwned + Clone, + C: CustomMsg, +{ +} + +pub trait Cw721Execute +where + T: Serialize + DeserializeOwned + Clone, + C: CustomMsg, +{ + type Err: ToString; + + fn transfer_nft( + &self, + deps: DepsMut, + env: Env, + info: MessageInfo, + recipient: String, + token_id: String, + ) -> Result, Self::Err>; + + fn send_nft( + &self, + deps: DepsMut, + env: Env, + info: MessageInfo, + contract: String, + token_id: String, + msg: Binary, + ) -> Result, Self::Err>; + + fn approve( + &self, + deps: DepsMut, + env: Env, + info: MessageInfo, + spender: String, + token_id: String, + expires: Option, + ) -> Result, Self::Err>; + + fn revoke( + &self, + deps: DepsMut, + env: Env, + info: MessageInfo, + spender: String, + token_id: String, + ) -> Result, Self::Err>; + + fn approve_all( + &self, + deps: DepsMut, + env: Env, + info: MessageInfo, + operator: String, + expires: Option, + ) -> Result, Self::Err>; + + fn revoke_all( + &self, + deps: DepsMut, + env: Env, + info: MessageInfo, + operator: String, + ) -> Result, Self::Err>; + + fn burn( + &self, + deps: DepsMut, + env: Env, + info: MessageInfo, + token_id: String, + ) -> Result, Self::Err>; +} + +pub trait Cw721Query +where + T: Serialize + DeserializeOwned + Clone, +{ + // TODO: use custom error? + // How to handle the two derived error types? + + fn contract_info(&self, deps: Deps) -> StdResult; + + fn num_tokens(&self, deps: Deps) -> StdResult; + + fn nft_info(&self, deps: Deps, token_id: String) -> StdResult>; + + fn owner_of( + &self, + deps: Deps, + env: Env, + token_id: String, + include_expired: bool, + ) -> StdResult; + + fn operators( + &self, + deps: Deps, + env: Env, + owner: String, + include_expired: bool, + start_after: Option, + limit: Option, + ) -> StdResult; + + fn approval( + &self, + deps: Deps, + env: Env, + token_id: String, + spender: String, + include_expired: bool, + ) -> StdResult; + + fn approvals( + &self, + deps: Deps, + env: Env, + token_id: String, + include_expired: bool, + ) -> StdResult; + + fn tokens( + &self, + deps: Deps, + owner: String, + start_after: Option, + limit: Option, + ) -> StdResult; + + fn all_tokens( + &self, + deps: Deps, + start_after: Option, + limit: Option, + ) -> StdResult; + + fn all_nft_info( + &self, + deps: Deps, + env: Env, + token_id: String, + include_expired: bool, + ) -> StdResult>; +} diff --git a/packages/paloma_cosmwasm/examples/schema.rs b/packages/paloma_cosmwasm/examples/paloma_cosmwasm_schema.rs similarity index 100% rename from packages/paloma_cosmwasm/examples/schema.rs rename to packages/paloma_cosmwasm/examples/paloma_cosmwasm_schema.rs diff --git a/wormhole/README.md b/wormhole/README.md new file mode 100644 index 00000000..1fc828ac --- /dev/null +++ b/wormhole/README.md @@ -0,0 +1,136 @@ +# Terra Wormhole Contract Deployment + +This readme describes the steps for building, verifying, and deploying Terra smart contracts for Wormhole. + +**WARNING**: *This process is only Linux host compatible at this time.* + +## Verify Tilt + +Before building Terra contracts, ensure that the specific commit you will be +building from passes in tilt. This that ensures basic functionality of the +Terra smart contracts that you are about to build and deploy. + +## Build Contracts + +The following command can be used to build Terra contracts via Docker. + +Build Target Options: [`mainnet`|`testnet`|`devnet`| + +These network names correspond to the naming convention used by wormhole +elsewhere. This means that `mainnet` corresponds to Terra `mainnet`, +`testnet` corresponds to Terra `testnet`, and `devnet` is `localterra`. + +```console +wormhole/terra $ make artifacts +``` + +Upon completion, the compiled bytecode for the Terra contracts will be placed +into the `artifacts` directory. + +## Verify Checksums + +Now that you have built the Terra contracts, you should ask a peer to build +using the same process and compare the equivalent checksums.txt files to make +sure the contract bytecode(s) are deterministic. + +```console +wormhole/terra $ cat artifacts/checksums.txt +``` + +Once you have verified the Terra contracts are deterministic with a peer, you can now move to the deploy step. + +## Run tests + +**Disclaimer: Currently the only test that exists is for the token bridge's transfer.** + +You can run the integration test suite on the artifacts you built. + +```console +wormhole/terra $ make test +``` + +This command deploys your artifacts and performs various interactions with your +contracts in a LocalTerra node. Any new functionality (including expected errors) +to the contracts should be added to this test suite. + +## Deploy Contracts + +Now that you have built and verified checksums, you can now deploy one or more relevant contracts to the Terra blockchain. + +Deploy Target Options: [`mainnet`|`testnet`|`devnet`] + +You will need to define a `payer-DEPLOY_TARGET.json` for the relevant deploy +target (eg. `payer-testnet.json`). This will contain the relevant wallet +private key that you will be using to deploy the contracts. + +```console +wormhole/terra $ make deploy/bridge +wormhole/terra $ make deploy/token_bridge +wormhole/terra $ make deploy/nft_bridge +``` + +For each deployed contract, you will get a code id for that relevant +contract for the deployment, make note of these so you can use them in +the next step for on-chain verification. + +## Verify On-Chain + +Now that you have deployed one or more contracts on-chain, you can verify the +onchain bytecode and make sure it matches the same checksums you identified +above. + +For each contract you wish to verify on-chain, you will need the following elements: + +- Path to the contracts bytecode (eg. `artifacts-testnet/token_bridge.wasm`) +- Terra code id for the relevant contract (eg. `59614`) +- A network to verify on (`mainnet`, `testnet`, or `devnet`) + +Below is how to verify all three contracts: + +```console +wormhole/terra $ ./verify artifacts/wormhole.wasm NEW_BRIDGE_CODE_ID +wormhole/terra $ ./verify artifacts/token_bridge.wasm NEW_TOKEN_BRIDGE_CODE_ID +wormhole/terra $ ./verify artifacts/nft_bridge.wasm NEW_NFT_BRIDGE_CODE_ID +``` +Example: `./verify artifacts/token_bridge.wasm 59614` + +For each contract, you should expect a `Successfully verified` output message. +If all contracts can be successfully verified, you can engage in Wormhole +protocol governance to obtain an authorized VAA for the contract upgrade(s). + +A verification failure should never happen, and is a sign of some error in the +deployment process. Do not proceed with governance until you can verify the +on-chain bytecode with the locally compiled bytecode. + + +## Governance + +### Mainnet + +Upgrades on mainnet have to go through governance. Once the code is deployed in +the previous step, an unsigned governance VAA can be generated + +```sh +./generate_governance -m token_bridge -c 59614 > token-bridge-upgrade-59614.prototxt +``` + +This will write to the `token-bridge-upgrade-59614.prototxt` file, which can +now be shared with the guardians to vote on. + +Once the guardians have reached quorum, the VAA may be submitted from any +funded wallet: TODO - make this easier and more unified + +``` sh +node main.js terra execute_governance_vaa --rpc "https://lcd.terra.dev" --chain_id "columbus-5" --mnemonic "..." --token_bridge "terra10nmmwe8r3g99a9newtqa7a75xfgs2e8z87r2sf" +``` + +### Testnet + +For the contracts on testnet, the deployer wallet retains the upgrade +authority, so these don't have to go through governance. + +For example, to migrate the token bridge to 59614, run in `tools/`: + +``` sh +node migrate_testnet.js --code_id 59614 --contract terra1pseddrv0yfsn76u4zxrjmtf45kdlmalswdv39a --mnemonic "..." +``` diff --git a/wormhole/cw20-legacy/Cargo.toml b/wormhole/cw20-legacy/Cargo.toml new file mode 100644 index 00000000..3c47bf26 --- /dev/null +++ b/wormhole/cw20-legacy/Cargo.toml @@ -0,0 +1,33 @@ +[package] +name = "cw20-legacy" +version = "0.2.0" +authors = ["Mohammadsadegh Najafi "] +edition = "2018" +description = "Basic implementation of a CosmWasm-20 compliant token compatible with Columbus-4 token" +license = "Apache-2.0" +repository = "https://github.com/terra-money/cosmwasm-contracts" +homepage = "https://www.terra.money/" +documentation = "https://github.com/terra-money/cosmwasm-contracts/blob/bombay/contracts/terra-cw20/README.md" + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +backtraces = ["cosmwasm-std/backtraces"] +# use library feature to disable all instantiate/execute/query exports +library = [] + +[dependencies] +cosmwasm-std = "1.0.0" +cosmwasm-storage = { version = "1.0.0" } +cw-storage-plus = "0.13.4" +cw-utils = {version = "0.13.4"} +cw2 = "0.13.4" +cw20 = "0.13.4" +cw20-base = "0.13.4" +schemars = "0.8.10" +serde = { version = "1.0.137", default-features = false, features = ["derive"] } +thiserror = "1.0.31" + +[dev-dependencies] +cosmwasm-schema = "1.0.0" diff --git a/wormhole/cw20-legacy/NOTICE b/wormhole/cw20-legacy/NOTICE new file mode 100644 index 00000000..84b1c210 --- /dev/null +++ b/wormhole/cw20-legacy/NOTICE @@ -0,0 +1,14 @@ +CW20-Base: A reference implementation for fungible token on CosmWasm +Copyright (C) 2020 Confio OÜ + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/wormhole/cw20-legacy/README.md b/wormhole/cw20-legacy/README.md new file mode 100644 index 00000000..df9bd919 --- /dev/null +++ b/wormhole/cw20-legacy/README.md @@ -0,0 +1,53 @@ +# CW20-Legacy + +This version of the CW20 base contract is compatible with the legacy contract running on the Terra chain Columbus-4. + +> **NOTE** +This contract is modified for the purpose of migration from a Columbus-4 CW20 token contract to Columbus-5. Using the official version of CW20 is strongly recommended for other usages. + +This is a basic implementation of a cw20 contract. It implements +the [CW20 spec](../../packages/cw20/README.md) and is designed to +be deployed as is, or imported into other contracts to easily build +cw20-compatible tokens with custom logic. + +Implements: + +- [x] CW20 Base +- [x] Mintable extension +- [x] Allowances extension + +## Running this contract + +You will need Rust 1.44.1+ with `wasm32-unknown-unknown` target installed. + +You can run unit tests on this via: + +`cargo test` + +Once you are happy with the content, you can compile it to wasm via: + +``` +RUSTFLAGS='-C link-arg=-s' cargo wasm +cp ../../target/wasm32-unknown-unknown/release/cw20_base.wasm . +ls -l cw20_base.wasm +sha256sum cw20_base.wasm +``` + +Or for a production-ready (optimized) build, run a build command in the +the repository root: https://github.com/CosmWasm/cosmwasm-plus#compiling. + +## Importing this contract + +You can also import much of the logic of this contract to build another +ERC20-contract, such as a bonding curve, overiding or extending what you +need. + +Basically, you just need to write your handle function and import +`cw20_base::contract::handle_transfer`, etc and dispatch to them. +This allows you to use custom `ExecuteMsg` and `QueryMsg` with your additional +calls, but then use the underlying implementation for the standard cw20 +messages you want to support. The same with `QueryMsg`. You *could* reuse `instantiate` +as it, but it is likely you will want to change it. And it is rather simple. + +Look at [`cw20-staking`](../cw20-staking/README.md) for an example of how to "inherit" +all this token functionality and combine it with custom logic. diff --git a/wormhole/cw20-legacy/examples/cw20_legacy_schema.rs b/wormhole/cw20-legacy/examples/cw20_legacy_schema.rs new file mode 100644 index 00000000..0afc8144 --- /dev/null +++ b/wormhole/cw20-legacy/examples/cw20_legacy_schema.rs @@ -0,0 +1,26 @@ +use std::env::current_dir; +use std::fs::create_dir_all; + +use cosmwasm_schema::{export_schema, remove_schemas, schema_for}; + +use cw20::{ + AllAccountsResponse, AllAllowancesResponse, AllowanceResponse, BalanceResponse, + TokenInfoResponse, +}; +use cw20_base::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; + +fn main() { + let mut out_dir = current_dir().unwrap(); + out_dir.push("schema"); + create_dir_all(&out_dir).unwrap(); + remove_schemas(&out_dir).unwrap(); + + export_schema(&schema_for!(InstantiateMsg), &out_dir); + export_schema(&schema_for!(ExecuteMsg), &out_dir); + export_schema(&schema_for!(QueryMsg), &out_dir); + export_schema(&schema_for!(AllowanceResponse), &out_dir); + export_schema(&schema_for!(BalanceResponse), &out_dir); + export_schema(&schema_for!(TokenInfoResponse), &out_dir); + export_schema(&schema_for!(AllAllowancesResponse), &out_dir); + export_schema(&schema_for!(AllAccountsResponse), &out_dir); +} diff --git a/wormhole/cw20-legacy/schema/all_accounts_response.json b/wormhole/cw20-legacy/schema/all_accounts_response.json new file mode 100644 index 00000000..cea50fba --- /dev/null +++ b/wormhole/cw20-legacy/schema/all_accounts_response.json @@ -0,0 +1,16 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "AllAccountsResponse", + "type": "object", + "required": [ + "accounts" + ], + "properties": { + "accounts": { + "type": "array", + "items": { + "type": "string" + } + } + } +} diff --git a/wormhole/cw20-legacy/schema/all_allowances_response.json b/wormhole/cw20-legacy/schema/all_allowances_response.json new file mode 100644 index 00000000..6bb2291c --- /dev/null +++ b/wormhole/cw20-legacy/schema/all_allowances_response.json @@ -0,0 +1,99 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "AllAllowancesResponse", + "type": "object", + "required": [ + "allowances" + ], + "properties": { + "allowances": { + "type": "array", + "items": { + "$ref": "#/definitions/AllowanceInfo" + } + } + }, + "definitions": { + "AllowanceInfo": { + "type": "object", + "required": [ + "allowance", + "expires", + "spender" + ], + "properties": { + "allowance": { + "$ref": "#/definitions/Uint128" + }, + "expires": { + "$ref": "#/definitions/Expiration" + }, + "spender": { + "type": "string" + } + } + }, + "Expiration": { + "description": "Expiration represents a point in time when some event happens. It can compare with a BlockInfo and will return is_expired() == true once the condition is hit (and for every block in the future)", + "oneOf": [ + { + "description": "AtHeight will expire when `env.block.height` >= height", + "type": "object", + "required": [ + "at_height" + ], + "properties": { + "at_height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + { + "description": "AtTime will expire when `env.block.time` >= time", + "type": "object", + "required": [ + "at_time" + ], + "properties": { + "at_time": { + "$ref": "#/definitions/Timestamp" + } + }, + "additionalProperties": false + }, + { + "description": "Never will never expire. Used to express the empty variant", + "type": "object", + "required": [ + "never" + ], + "properties": { + "never": { + "type": "object" + } + }, + "additionalProperties": false + } + ] + }, + "Timestamp": { + "description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```", + "allOf": [ + { + "$ref": "#/definitions/Uint64" + } + ] + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + }, + "Uint64": { + "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```", + "type": "string" + } + } +} diff --git a/wormhole/cw20-legacy/schema/allowance_response.json b/wormhole/cw20-legacy/schema/allowance_response.json new file mode 100644 index 00000000..c4f98d6f --- /dev/null +++ b/wormhole/cw20-legacy/schema/allowance_response.json @@ -0,0 +1,81 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "AllowanceResponse", + "type": "object", + "required": [ + "allowance", + "expires" + ], + "properties": { + "allowance": { + "$ref": "#/definitions/Uint128" + }, + "expires": { + "$ref": "#/definitions/Expiration" + } + }, + "definitions": { + "Expiration": { + "description": "Expiration represents a point in time when some event happens. It can compare with a BlockInfo and will return is_expired() == true once the condition is hit (and for every block in the future)", + "oneOf": [ + { + "description": "AtHeight will expire when `env.block.height` >= height", + "type": "object", + "required": [ + "at_height" + ], + "properties": { + "at_height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + { + "description": "AtTime will expire when `env.block.time` >= time", + "type": "object", + "required": [ + "at_time" + ], + "properties": { + "at_time": { + "$ref": "#/definitions/Timestamp" + } + }, + "additionalProperties": false + }, + { + "description": "Never will never expire. Used to express the empty variant", + "type": "object", + "required": [ + "never" + ], + "properties": { + "never": { + "type": "object" + } + }, + "additionalProperties": false + } + ] + }, + "Timestamp": { + "description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```", + "allOf": [ + { + "$ref": "#/definitions/Uint64" + } + ] + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + }, + "Uint64": { + "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```", + "type": "string" + } + } +} diff --git a/wormhole/cw20-legacy/schema/balance_response.json b/wormhole/cw20-legacy/schema/balance_response.json new file mode 100644 index 00000000..4e1a0be2 --- /dev/null +++ b/wormhole/cw20-legacy/schema/balance_response.json @@ -0,0 +1,19 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "BalanceResponse", + "type": "object", + "required": [ + "balance" + ], + "properties": { + "balance": { + "$ref": "#/definitions/Uint128" + } + }, + "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/wormhole/cw20-legacy/schema/cw20_execute_msg.json b/wormhole/cw20-legacy/schema/cw20_execute_msg.json new file mode 100644 index 00000000..e4bc559c --- /dev/null +++ b/wormhole/cw20-legacy/schema/cw20_execute_msg.json @@ -0,0 +1,442 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Cw20ExecuteMsg", + "oneOf": [ + { + "description": "Transfer is a base message to move tokens to another account without triggering actions", + "type": "object", + "required": [ + "transfer" + ], + "properties": { + "transfer": { + "type": "object", + "required": [ + "amount", + "recipient" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "recipient": { + "type": "string" + } + } + } + }, + "additionalProperties": false + }, + { + "description": "Burn is a base message to destroy tokens forever", + "type": "object", + "required": [ + "burn" + ], + "properties": { + "burn": { + "type": "object", + "required": [ + "amount" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + } + } + } + }, + "additionalProperties": false + }, + { + "description": "Send is a base message to transfer tokens to a contract and trigger an action on the receiving contract.", + "type": "object", + "required": [ + "send" + ], + "properties": { + "send": { + "type": "object", + "required": [ + "amount", + "contract", + "msg" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "contract": { + "type": "string" + }, + "msg": { + "$ref": "#/definitions/Binary" + } + } + } + }, + "additionalProperties": false + }, + { + "description": "Only with \"approval\" extension. Allows spender to access an additional amount tokens from the owner's (env.sender) account. If expires is Some(), overwrites current allowance expiration with this one.", + "type": "object", + "required": [ + "increase_allowance" + ], + "properties": { + "increase_allowance": { + "type": "object", + "required": [ + "amount", + "spender" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "expires": { + "anyOf": [ + { + "$ref": "#/definitions/Expiration" + }, + { + "type": "null" + } + ] + }, + "spender": { + "type": "string" + } + } + } + }, + "additionalProperties": false + }, + { + "description": "Only with \"approval\" extension. Lowers the spender's access of tokens from the owner's (env.sender) account by amount. If expires is Some(), overwrites current allowance expiration with this one.", + "type": "object", + "required": [ + "decrease_allowance" + ], + "properties": { + "decrease_allowance": { + "type": "object", + "required": [ + "amount", + "spender" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "expires": { + "anyOf": [ + { + "$ref": "#/definitions/Expiration" + }, + { + "type": "null" + } + ] + }, + "spender": { + "type": "string" + } + } + } + }, + "additionalProperties": false + }, + { + "description": "Only with \"approval\" extension. Transfers amount tokens from owner -> recipient if `env.sender` has sufficient pre-approval.", + "type": "object", + "required": [ + "transfer_from" + ], + "properties": { + "transfer_from": { + "type": "object", + "required": [ + "amount", + "owner", + "recipient" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "owner": { + "type": "string" + }, + "recipient": { + "type": "string" + } + } + } + }, + "additionalProperties": false + }, + { + "description": "Only with \"approval\" extension. Sends amount tokens from owner -> contract if `env.sender` has sufficient pre-approval.", + "type": "object", + "required": [ + "send_from" + ], + "properties": { + "send_from": { + "type": "object", + "required": [ + "amount", + "contract", + "msg", + "owner" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "contract": { + "type": "string" + }, + "msg": { + "$ref": "#/definitions/Binary" + }, + "owner": { + "type": "string" + } + } + } + }, + "additionalProperties": false + }, + { + "description": "Only with \"approval\" extension. Destroys tokens forever", + "type": "object", + "required": [ + "burn_from" + ], + "properties": { + "burn_from": { + "type": "object", + "required": [ + "amount", + "owner" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "owner": { + "type": "string" + } + } + } + }, + "additionalProperties": false + }, + { + "description": "Only with the \"mintable\" extension. If authorized, creates amount new tokens and adds to the recipient balance.", + "type": "object", + "required": [ + "mint" + ], + "properties": { + "mint": { + "type": "object", + "required": [ + "amount", + "recipient" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "recipient": { + "type": "string" + } + } + } + }, + "additionalProperties": false + }, + { + "description": "Only with the \"marketing\" extension. If authorized, updates marketing metadata. Setting None/null for any of these will leave it unchanged. Setting Some(\"\") will clear this field on the contract storage", + "type": "object", + "required": [ + "update_marketing" + ], + "properties": { + "update_marketing": { + "type": "object", + "properties": { + "description": { + "description": "A longer description of the token and it's utility. Designed for tooltips or such", + "type": [ + "string", + "null" + ] + }, + "marketing": { + "description": "The address (if any) who can update this data structure", + "type": [ + "string", + "null" + ] + }, + "project": { + "description": "A URL pointing to the project behind this token.", + "type": [ + "string", + "null" + ] + } + } + } + }, + "additionalProperties": false + }, + { + "description": "If set as the \"marketing\" role on the contract, upload a new URL, SVG, or PNG for the token", + "type": "object", + "required": [ + "upload_logo" + ], + "properties": { + "upload_logo": { + "$ref": "#/definitions/Logo" + } + }, + "additionalProperties": false + } + ], + "definitions": { + "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", + "type": "string" + }, + "EmbeddedLogo": { + "description": "This is used to store the logo on the blockchain in an accepted format. Enforce maximum size of 5KB on all variants.", + "oneOf": [ + { + "description": "Store the Logo as an SVG file. The content must conform to the spec at https://en.wikipedia.org/wiki/Scalable_Vector_Graphics (The contract should do some light-weight sanity-check validation)", + "type": "object", + "required": [ + "svg" + ], + "properties": { + "svg": { + "$ref": "#/definitions/Binary" + } + }, + "additionalProperties": false + }, + { + "description": "Store the Logo as a PNG file. This will likely only support up to 64x64 or so within the 5KB limit.", + "type": "object", + "required": [ + "png" + ], + "properties": { + "png": { + "$ref": "#/definitions/Binary" + } + }, + "additionalProperties": false + } + ] + }, + "Expiration": { + "description": "Expiration represents a point in time when some event happens. It can compare with a BlockInfo and will return is_expired() == true once the condition is hit (and for every block in the future)", + "oneOf": [ + { + "description": "AtHeight will expire when `env.block.height` >= height", + "type": "object", + "required": [ + "at_height" + ], + "properties": { + "at_height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + { + "description": "AtTime will expire when `env.block.time` >= time", + "type": "object", + "required": [ + "at_time" + ], + "properties": { + "at_time": { + "$ref": "#/definitions/Timestamp" + } + }, + "additionalProperties": false + }, + { + "description": "Never will never expire. Used to express the empty variant", + "type": "object", + "required": [ + "never" + ], + "properties": { + "never": { + "type": "object" + } + }, + "additionalProperties": false + } + ] + }, + "Logo": { + "description": "This is used for uploading logo data, or setting it in InstantiateData", + "oneOf": [ + { + "description": "A reference to an externally hosted logo. Must be a valid HTTP or HTTPS URL.", + "type": "object", + "required": [ + "url" + ], + "properties": { + "url": { + "type": "string" + } + }, + "additionalProperties": false + }, + { + "description": "Logo content stored on the blockchain. Enforce maximum size of 5KB on all variants", + "type": "object", + "required": [ + "embedded" + ], + "properties": { + "embedded": { + "$ref": "#/definitions/EmbeddedLogo" + } + }, + "additionalProperties": false + } + ] + }, + "Timestamp": { + "description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```", + "allOf": [ + { + "$ref": "#/definitions/Uint64" + } + ] + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + }, + "Uint64": { + "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```", + "type": "string" + } + } +} diff --git a/wormhole/cw20-legacy/schema/instantiate_msg.json b/wormhole/cw20-legacy/schema/instantiate_msg.json new file mode 100644 index 00000000..7b9aa1ee --- /dev/null +++ b/wormhole/cw20-legacy/schema/instantiate_msg.json @@ -0,0 +1,192 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "InstantiateMsg", + "type": "object", + "required": [ + "decimals", + "initial_balances", + "name", + "symbol" + ], + "properties": { + "decimals": { + "type": "integer", + "format": "uint8", + "minimum": 0.0 + }, + "initial_balances": { + "type": "array", + "items": { + "$ref": "#/definitions/Cw20Coin" + } + }, + "marketing": { + "anyOf": [ + { + "$ref": "#/definitions/InstantiateMarketingInfo" + }, + { + "type": "null" + } + ] + }, + "mint": { + "anyOf": [ + { + "$ref": "#/definitions/MinterResponse" + }, + { + "type": "null" + } + ] + }, + "name": { + "type": "string" + }, + "symbol": { + "type": "string" + } + }, + "definitions": { + "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", + "type": "string" + }, + "Cw20Coin": { + "type": "object", + "required": [ + "address", + "amount" + ], + "properties": { + "address": { + "type": "string" + }, + "amount": { + "$ref": "#/definitions/Uint128" + } + } + }, + "EmbeddedLogo": { + "description": "This is used to store the logo on the blockchain in an accepted format. Enforce maximum size of 5KB on all variants.", + "oneOf": [ + { + "description": "Store the Logo as an SVG file. The content must conform to the spec at https://en.wikipedia.org/wiki/Scalable_Vector_Graphics (The contract should do some light-weight sanity-check validation)", + "type": "object", + "required": [ + "svg" + ], + "properties": { + "svg": { + "$ref": "#/definitions/Binary" + } + }, + "additionalProperties": false + }, + { + "description": "Store the Logo as a PNG file. This will likely only support up to 64x64 or so within the 5KB limit.", + "type": "object", + "required": [ + "png" + ], + "properties": { + "png": { + "$ref": "#/definitions/Binary" + } + }, + "additionalProperties": false + } + ] + }, + "InstantiateMarketingInfo": { + "type": "object", + "properties": { + "description": { + "type": [ + "string", + "null" + ] + }, + "logo": { + "anyOf": [ + { + "$ref": "#/definitions/Logo" + }, + { + "type": "null" + } + ] + }, + "marketing": { + "type": [ + "string", + "null" + ] + }, + "project": { + "type": [ + "string", + "null" + ] + } + } + }, + "Logo": { + "description": "This is used for uploading logo data, or setting it in InstantiateData", + "oneOf": [ + { + "description": "A reference to an externally hosted logo. Must be a valid HTTP or HTTPS URL.", + "type": "object", + "required": [ + "url" + ], + "properties": { + "url": { + "type": "string" + } + }, + "additionalProperties": false + }, + { + "description": "Logo content stored on the blockchain. Enforce maximum size of 5KB on all variants", + "type": "object", + "required": [ + "embedded" + ], + "properties": { + "embedded": { + "$ref": "#/definitions/EmbeddedLogo" + } + }, + "additionalProperties": false + } + ] + }, + "MinterResponse": { + "type": "object", + "required": [ + "minter" + ], + "properties": { + "cap": { + "description": "cap is a hard cap on total supply that can be achieved by minting. Note that this refers to total_supply. If None, there is unlimited cap.", + "anyOf": [ + { + "$ref": "#/definitions/Uint128" + }, + { + "type": "null" + } + ] + }, + "minter": { + "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/wormhole/cw20-legacy/schema/query_msg.json b/wormhole/cw20-legacy/schema/query_msg.json new file mode 100644 index 00000000..7c33c352 --- /dev/null +++ b/wormhole/cw20-legacy/schema/query_msg.json @@ -0,0 +1,168 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "QueryMsg", + "oneOf": [ + { + "description": "Returns the current balance of the given address, 0 if unset. Return type: BalanceResponse.", + "type": "object", + "required": [ + "balance" + ], + "properties": { + "balance": { + "type": "object", + "required": [ + "address" + ], + "properties": { + "address": { + "type": "string" + } + } + } + }, + "additionalProperties": false + }, + { + "description": "Returns metadata on the contract - name, decimals, supply, etc. Return type: TokenInfoResponse.", + "type": "object", + "required": [ + "token_info" + ], + "properties": { + "token_info": { + "type": "object" + } + }, + "additionalProperties": false + }, + { + "description": "Only with \"mintable\" extension. Returns who can mint and the hard cap on maximum tokens after minting. Return type: MinterResponse.", + "type": "object", + "required": [ + "minter" + ], + "properties": { + "minter": { + "type": "object" + } + }, + "additionalProperties": false + }, + { + "description": "Only with \"allowance\" extension. Returns how much spender can use from owner account, 0 if unset. Return type: AllowanceResponse.", + "type": "object", + "required": [ + "allowance" + ], + "properties": { + "allowance": { + "type": "object", + "required": [ + "owner", + "spender" + ], + "properties": { + "owner": { + "type": "string" + }, + "spender": { + "type": "string" + } + } + } + }, + "additionalProperties": false + }, + { + "description": "Only with \"enumerable\" extension (and \"allowances\") Returns all allowances this owner has approved. Supports pagination. Return type: AllAllowancesResponse.", + "type": "object", + "required": [ + "all_allowances" + ], + "properties": { + "all_allowances": { + "type": "object", + "required": [ + "owner" + ], + "properties": { + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "owner": { + "type": "string" + }, + "start_after": { + "type": [ + "string", + "null" + ] + } + } + } + }, + "additionalProperties": false + }, + { + "description": "Only with \"enumerable\" extension Returns all accounts that have balances. Supports pagination. Return type: AllAccountsResponse.", + "type": "object", + "required": [ + "all_accounts" + ], + "properties": { + "all_accounts": { + "type": "object", + "properties": { + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "type": [ + "string", + "null" + ] + } + } + } + }, + "additionalProperties": false + }, + { + "description": "Only with \"marketing\" extension Returns more metadata on the contract to display in the client: - description, logo, project url, etc. Return type: MarketingInfoResponse", + "type": "object", + "required": [ + "marketing_info" + ], + "properties": { + "marketing_info": { + "type": "object" + } + }, + "additionalProperties": false + }, + { + "description": "Only with \"marketing\" extension Downloads the embedded logo data (if stored on chain). Errors if no logo data is stored for this contract. Return type: DownloadLogoResponse.", + "type": "object", + "required": [ + "download_logo" + ], + "properties": { + "download_logo": { + "type": "object" + } + }, + "additionalProperties": false + } + ] +} diff --git a/wormhole/cw20-legacy/schema/token_info_response.json b/wormhole/cw20-legacy/schema/token_info_response.json new file mode 100644 index 00000000..9920c841 --- /dev/null +++ b/wormhole/cw20-legacy/schema/token_info_response.json @@ -0,0 +1,33 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "TokenInfoResponse", + "type": "object", + "required": [ + "decimals", + "name", + "symbol", + "total_supply" + ], + "properties": { + "decimals": { + "type": "integer", + "format": "uint8", + "minimum": 0.0 + }, + "name": { + "type": "string" + }, + "symbol": { + "type": "string" + }, + "total_supply": { + "$ref": "#/definitions/Uint128" + } + }, + "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/wormhole/cw20-legacy/src/allowances.rs b/wormhole/cw20-legacy/src/allowances.rs new file mode 100644 index 00000000..82f8ed8c --- /dev/null +++ b/wormhole/cw20-legacy/src/allowances.rs @@ -0,0 +1,795 @@ +use cosmwasm_std::{ + attr, Addr, Api, Binary, BlockInfo, Deps, DepsMut, Env, MessageInfo, Response, StdError, + StdResult, Storage, Uint128, +}; +use cw20::{AllowanceResponse, Cw20ReceiveMsg, Expiration}; + +use crate::error::ContractError; +use crate::state::{ALLOWANCES, BALANCES, TOKEN_INFO}; + +pub fn execute_increase_allowance( + deps: DepsMut, + _env: Env, + info: MessageInfo, + spender: String, + amount: Uint128, + expires: Option, +) -> Result { + let spender_addr = deps.api.addr_validate(&spender)?; + if spender_addr == info.sender { + return Err(ContractError::CannotSetOwnAccount {}); + } + + ALLOWANCES.update( + deps.storage, + ( + deps.api + .addr_canonicalize(&info.sender.to_string())? + .as_slice(), + deps.api + .addr_canonicalize(&spender_addr.to_string())? + .as_slice(), + ), + |allow| -> StdResult<_> { + let mut val = allow.unwrap_or_default(); + if let Some(exp) = expires { + val.expires = exp; + } + val.allowance += amount; + Ok(val) + }, + )?; + + Ok(Response::new().add_attributes(vec![ + attr("action", "increase_allowance"), + attr("owner", info.sender), + attr("spender", spender), + attr("amount", amount), + ])) +} + +pub fn execute_decrease_allowance( + deps: DepsMut, + _env: Env, + info: MessageInfo, + spender: String, + amount: Uint128, + expires: Option, +) -> Result { + let spender_addr = deps.api.addr_validate(&spender)?; + if spender_addr == info.sender { + return Err(ContractError::CannotSetOwnAccount {}); + } + + let sender_raw = deps.api.addr_canonicalize(info.sender.as_str())?; + let spender_raw = deps.api.addr_canonicalize(spender.as_str())?; + + let key = (sender_raw.as_slice(), spender_raw.as_slice()); + // load value and delete if it hits 0, or update otherwise + let mut allowance = ALLOWANCES.load(deps.storage, key)?; + if amount < allowance.allowance { + // update the new amount + allowance.allowance = allowance + .allowance + .checked_sub(amount) + .map_err(StdError::overflow)?; + if let Some(exp) = expires { + allowance.expires = exp; + } + ALLOWANCES.save(deps.storage, key, &allowance)?; + } else { + ALLOWANCES.remove(deps.storage, key); + } + + Ok(Response::new().add_attributes(vec![ + attr("action", "decrease_allowance"), + attr("owner", info.sender), + attr("spender", spender), + attr("amount", amount), + ])) +} + +// this can be used to update a lower allowance - call bucket.update with proper keys +pub fn deduct_allowance( + storage: &mut dyn Storage, + api: &dyn Api, + owner: &Addr, + spender: &Addr, + block: &BlockInfo, + amount: Uint128, +) -> Result { + ALLOWANCES.update( + storage, + ( + api.addr_canonicalize(owner.as_str())?.as_slice(), + api.addr_canonicalize(spender.as_str())?.as_slice(), + ), + |current| { + match current { + Some(mut a) => { + if a.expires.is_expired(block) { + Err(ContractError::Expired {}) + } else { + // deduct the allowance if enough + a.allowance = a + .allowance + .checked_sub(amount) + .map_err(StdError::overflow)?; + Ok(a) + } + } + None => Err(ContractError::NoAllowance {}), + } + }, + ) +} + +pub fn execute_transfer_from( + deps: DepsMut, + env: Env, + info: MessageInfo, + owner: String, + recipient: String, + amount: Uint128, +) -> Result { + let rcpt_addr = deps.api.addr_validate(&recipient)?; + let owner_addr = deps.api.addr_validate(&owner)?; + + // deduct allowance before doing anything else have enough allowance + deduct_allowance( + deps.storage, + deps.api, + &owner_addr, + &info.sender, + &env.block, + amount, + )?; + + BALANCES.update( + deps.storage, + deps.api + .addr_canonicalize(&owner_addr.to_string())? + .as_slice(), + |balance: Option| -> StdResult<_> { + Ok(balance.unwrap_or_default().checked_sub(amount)?) + }, + )?; + BALANCES.update( + deps.storage, + deps.api + .addr_canonicalize(&rcpt_addr.to_string())? + .as_slice(), + |balance: Option| -> StdResult<_> { Ok(balance.unwrap_or_default() + amount) }, + )?; + + Ok(Response::new().add_attributes(vec![ + attr("action", "transfer_from"), + attr("from", owner), + attr("to", recipient), + attr("by", info.sender), + attr("amount", amount), + ])) +} + +pub fn execute_burn_from( + deps: DepsMut, + + env: Env, + info: MessageInfo, + owner: String, + amount: Uint128, +) -> Result { + let owner_addr = deps.api.addr_validate(&owner)?; + + // deduct allowance before doing anything else have enough allowance + deduct_allowance( + deps.storage, + deps.api, + &owner_addr, + &info.sender, + &env.block, + amount, + )?; + + // lower balance + BALANCES.update( + deps.storage, + deps.api + .addr_canonicalize(&owner_addr.to_string())? + .as_slice(), + |balance: Option| -> StdResult<_> { + Ok(balance.unwrap_or_default().checked_sub(amount)?) + }, + )?; + // reduce total_supply + TOKEN_INFO.update(deps.storage, |mut meta| -> StdResult<_> { + meta.total_supply = meta.total_supply.checked_sub(amount)?; + Ok(meta) + })?; + + Ok(Response::new().add_attributes(vec![ + attr("action", "burn_from"), + attr("from", owner), + attr("by", info.sender), + attr("amount", amount), + ])) +} + +pub fn execute_send_from( + deps: DepsMut, + env: Env, + info: MessageInfo, + owner: String, + contract: String, + amount: Uint128, + msg: Binary, +) -> Result { + let rcpt_addr = deps.api.addr_validate(&contract)?; + let owner_addr = deps.api.addr_validate(&owner)?; + + // deduct allowance before doing anything else have enough allowance + deduct_allowance( + deps.storage, + deps.api, + &owner_addr, + &info.sender, + &env.block, + amount, + )?; + + // move the tokens to the contract + BALANCES.update( + deps.storage, + deps.api + .addr_canonicalize(&owner_addr.to_string())? + .as_slice(), + |balance: Option| -> StdResult<_> { + Ok(balance.unwrap_or_default().checked_sub(amount)?) + }, + )?; + BALANCES.update( + deps.storage, + deps.api + .addr_canonicalize(&rcpt_addr.to_string())? + .as_slice(), + |balance: Option| -> StdResult<_> { Ok(balance.unwrap_or_default() + amount) }, + )?; + + let attrs = vec![ + attr("action", "send_from"), + attr("from", &owner), + attr("to", &contract), + attr("by", &info.sender), + attr("amount", amount), + ]; + + Ok(Response::new().add_attributes(attrs).add_message( + // create a send message + Cw20ReceiveMsg { + sender: info.sender.into(), + amount, + msg, + } + .into_cosmos_msg(contract)?, + )) +} + +pub fn query_allowance(deps: Deps, owner: String, spender: String) -> StdResult { + let owner_addr = deps.api.addr_validate(&owner)?; + let spender_addr = deps.api.addr_validate(&spender)?; + let allowance = ALLOWANCES + .may_load( + deps.storage, + ( + deps.api.addr_canonicalize(owner_addr.as_str())?.as_slice(), + deps.api + .addr_canonicalize(spender_addr.as_str())? + .as_slice(), + ), + )? + .unwrap_or_default(); + Ok(allowance) +} + +#[cfg(test)] +mod tests { + use super::*; + + use cosmwasm_std::testing::{mock_dependencies_with_balance, mock_env, mock_info}; + use cosmwasm_std::{coins, CosmosMsg, SubMsg, Timestamp, WasmMsg}; + use cw20::{Cw20Coin, TokenInfoResponse}; + + use crate::contract::{execute, instantiate, query_balance, query_token_info}; + use crate::msg::{ExecuteMsg, InstantiateMsg}; + + fn get_balance>(deps: Deps, address: T) -> Uint128 { + query_balance(deps, address.into()).unwrap().balance + } + + // this will set up the instantiation for other tests + fn do_instantiate>( + mut deps: DepsMut, + addr: T, + amount: Uint128, + ) -> TokenInfoResponse { + let instantiate_msg = InstantiateMsg { + name: "Auto Gen".to_string(), + symbol: "AUTO".to_string(), + decimals: 3, + initial_balances: vec![Cw20Coin { + address: addr.into(), + amount, + }], + mint: None, + }; + let info = mock_info("creator", &[]); + let env = mock_env(); + instantiate(deps.branch(), env, info, instantiate_msg).unwrap(); + query_token_info(deps.as_ref()).unwrap() + } + + #[test] + fn increase_decrease_allowances() { + let mut deps = mock_dependencies_with_balance(&coins(2, "token")); + + let owner = String::from("addr0001"); + let spender = String::from("addr0002"); + let info = mock_info(owner.as_ref(), &[]); + let env = mock_env(); + do_instantiate(deps.as_mut(), owner.clone(), Uint128::from(12340000u128)); + + // no allowance to start + let allowance = query_allowance(deps.as_ref(), owner.clone(), spender.clone()).unwrap(); + assert_eq!(allowance, AllowanceResponse::default()); + + // set allowance with height expiration + let allow1 = Uint128::from(7777u128); + let expires = Expiration::AtHeight(5432); + let msg = ExecuteMsg::IncreaseAllowance { + spender: spender.clone(), + amount: allow1, + expires: Some(expires), + }; + execute(deps.as_mut(), env.clone(), info.clone(), msg).unwrap(); + + // ensure it looks good + let allowance = query_allowance(deps.as_ref(), owner.clone(), spender.clone()).unwrap(); + assert_eq!( + allowance, + AllowanceResponse { + allowance: allow1, + expires + } + ); + + // decrease it a bit with no expire set - stays the same + let lower = Uint128::from(4444u128); + let allow2 = allow1.checked_sub(lower).unwrap(); + let msg = ExecuteMsg::DecreaseAllowance { + spender: spender.clone(), + amount: lower, + expires: None, + }; + execute(deps.as_mut(), env.clone(), info.clone(), msg).unwrap(); + let allowance = query_allowance(deps.as_ref(), owner.clone(), spender.clone()).unwrap(); + assert_eq!( + allowance, + AllowanceResponse { + allowance: allow2, + expires + } + ); + + // increase it some more and override the expires + let raise = Uint128::from(87654u128); + let allow3 = allow2 + raise; + let new_expire = Expiration::AtTime(Timestamp::from_seconds(8888888888)); + let msg = ExecuteMsg::IncreaseAllowance { + spender: spender.clone(), + amount: raise, + expires: Some(new_expire), + }; + execute(deps.as_mut(), env.clone(), info.clone(), msg).unwrap(); + let allowance = query_allowance(deps.as_ref(), owner.clone(), spender.clone()).unwrap(); + assert_eq!( + allowance, + AllowanceResponse { + allowance: allow3, + expires: new_expire + } + ); + + // decrease it below 0 + let msg = ExecuteMsg::DecreaseAllowance { + spender: spender.clone(), + amount: Uint128::from(99988647623876347u128), + expires: None, + }; + execute(deps.as_mut(), env, info, msg).unwrap(); + let allowance = query_allowance(deps.as_ref(), owner, spender).unwrap(); + assert_eq!(allowance, AllowanceResponse::default()); + } + + #[test] + fn allowances_independent() { + let mut deps = mock_dependencies_with_balance(&coins(2, "token")); + + let owner = String::from("addr0001"); + let spender = String::from("addr0002"); + let spender2 = String::from("addr0003"); + let info = mock_info(owner.as_ref(), &[]); + let env = mock_env(); + do_instantiate(deps.as_mut(), &owner, Uint128::from(12340000u128)); + + // no allowance to start + assert_eq!( + query_allowance(deps.as_ref(), owner.clone(), spender.clone()).unwrap(), + AllowanceResponse::default() + ); + assert_eq!( + query_allowance(deps.as_ref(), owner.clone(), spender2.clone()).unwrap(), + AllowanceResponse::default() + ); + assert_eq!( + query_allowance(deps.as_ref(), spender.clone(), spender2.clone()).unwrap(), + AllowanceResponse::default() + ); + + // set allowance with height expiration + let allow1 = Uint128::from(7777u128); + let expires = Expiration::AtHeight(5432); + let msg = ExecuteMsg::IncreaseAllowance { + spender: spender.clone(), + amount: allow1, + expires: Some(expires), + }; + execute(deps.as_mut(), env.clone(), info.clone(), msg).unwrap(); + + // set other allowance with no expiration + let allow2 = Uint128::from(87654u128); + let msg = ExecuteMsg::IncreaseAllowance { + spender: spender2.clone(), + amount: allow2, + expires: None, + }; + execute(deps.as_mut(), env, info, msg).unwrap(); + + // check they are proper + let expect_one = AllowanceResponse { + allowance: allow1, + expires, + }; + let expect_two = AllowanceResponse { + allowance: allow2, + expires: Expiration::Never {}, + }; + assert_eq!( + query_allowance(deps.as_ref(), owner.clone(), spender.clone()).unwrap(), + expect_one + ); + assert_eq!( + query_allowance(deps.as_ref(), owner.clone(), spender2.clone()).unwrap(), + expect_two + ); + assert_eq!( + query_allowance(deps.as_ref(), spender.clone(), spender2.clone()).unwrap(), + AllowanceResponse::default() + ); + + // also allow spender -> spender2 with no interference + let info = mock_info(spender.as_ref(), &[]); + let env = mock_env(); + let allow3 = Uint128::from(1821u128); + let expires3 = Expiration::AtTime(Timestamp::from_seconds(3767626296)); + let msg = ExecuteMsg::IncreaseAllowance { + spender: spender2.clone(), + amount: allow3, + expires: Some(expires3), + }; + execute(deps.as_mut(), env, info, msg).unwrap(); + let expect_three = AllowanceResponse { + allowance: allow3, + expires: expires3, + }; + assert_eq!( + query_allowance(deps.as_ref(), owner.clone(), spender.clone()).unwrap(), + expect_one + ); + assert_eq!( + query_allowance(deps.as_ref(), owner, spender2.clone()).unwrap(), + expect_two + ); + assert_eq!( + query_allowance(deps.as_ref(), spender, spender2).unwrap(), + expect_three + ); + } + + #[test] + fn no_self_allowance() { + let mut deps = mock_dependencies_with_balance(&coins(2, "token")); + + let owner = String::from("addr0001"); + let info = mock_info(owner.as_ref(), &[]); + let env = mock_env(); + do_instantiate(deps.as_mut(), &owner, Uint128::from(12340000u128)); + + // self-allowance + let msg = ExecuteMsg::IncreaseAllowance { + spender: owner.clone(), + amount: Uint128::from(7777u128), + expires: None, + }; + let err = execute(deps.as_mut(), env.clone(), info.clone(), msg).unwrap_err(); + assert_eq!(err, ContractError::CannotSetOwnAccount {}); + + // decrease self-allowance + let msg = ExecuteMsg::DecreaseAllowance { + spender: owner, + amount: Uint128::from(7777u128), + expires: None, + }; + let err = execute(deps.as_mut(), env, info, msg).unwrap_err(); + assert_eq!(err, ContractError::CannotSetOwnAccount {}); + } + + #[test] + fn transfer_from_respects_limits() { + let mut deps = mock_dependencies_with_balance(&[]); + let owner = String::from("addr0001"); + let spender = String::from("addr0002"); + let rcpt = String::from("addr0003"); + + let start = Uint128::from(999999u128); + do_instantiate(deps.as_mut(), &owner, start); + + // provide an allowance + let allow1 = Uint128::from(77777u128); + let msg = ExecuteMsg::IncreaseAllowance { + spender: spender.clone(), + amount: allow1, + expires: None, + }; + let info = mock_info(owner.as_ref(), &[]); + let env = mock_env(); + execute(deps.as_mut(), env, info, msg).unwrap(); + + // valid transfer of part of the allowance + let transfer = Uint128::from(44444u128); + let msg = ExecuteMsg::TransferFrom { + owner: owner.clone(), + recipient: rcpt.clone(), + amount: transfer, + }; + let info = mock_info(spender.as_ref(), &[]); + let env = mock_env(); + let res = execute(deps.as_mut(), env, info, msg).unwrap(); + assert_eq!(res.attributes[0], attr("action", "transfer_from")); + + // make sure money arrived + assert_eq!( + get_balance(deps.as_ref(), owner.clone()), + start.checked_sub(transfer).unwrap() + ); + assert_eq!(get_balance(deps.as_ref(), rcpt.clone()), transfer); + + // ensure it looks good + let allowance = query_allowance(deps.as_ref(), owner.clone(), spender.clone()).unwrap(); + let expect = AllowanceResponse { + allowance: allow1.checked_sub(transfer).unwrap(), + expires: Expiration::Never {}, + }; + assert_eq!(expect, allowance); + + // cannot send more than the allowance + let msg = ExecuteMsg::TransferFrom { + owner: owner.clone(), + recipient: rcpt.clone(), + amount: Uint128::from(33443u128), + }; + let info = mock_info(spender.as_ref(), &[]); + let env = mock_env(); + let err = execute(deps.as_mut(), env, info, msg).unwrap_err(); + assert!(matches!(err, ContractError::Std(StdError::Overflow { .. }))); + + // let us increase limit, but set the expiration (default env height is 12_345) + let info = mock_info(owner.as_ref(), &[]); + let env = mock_env(); + let msg = ExecuteMsg::IncreaseAllowance { + spender: spender.clone(), + amount: Uint128::from(1000u128), + expires: Some(Expiration::AtHeight(env.block.height)), + }; + execute(deps.as_mut(), env, info, msg).unwrap(); + + // we should now get the expiration error + let msg = ExecuteMsg::TransferFrom { + owner, + recipient: rcpt, + amount: Uint128::from(33443u128), + }; + let info = mock_info(spender.as_ref(), &[]); + let env = mock_env(); + let err = execute(deps.as_mut(), env, info, msg).unwrap_err(); + assert_eq!(err, ContractError::Expired {}); + } + + #[test] + fn burn_from_respects_limits() { + let mut deps = mock_dependencies_with_balance(&[]); + let owner = String::from("addr0001"); + let spender = String::from("addr0002"); + + let start = Uint128::from(999999u128); + do_instantiate(deps.as_mut(), &owner, start); + + // provide an allowance + let allow1 = Uint128::from(77777u128); + let msg = ExecuteMsg::IncreaseAllowance { + spender: spender.clone(), + amount: allow1, + expires: None, + }; + let info = mock_info(owner.as_ref(), &[]); + let env = mock_env(); + execute(deps.as_mut(), env, info, msg).unwrap(); + + // valid burn of part of the allowance + let transfer = Uint128::from(44444u128); + let msg = ExecuteMsg::BurnFrom { + owner: owner.clone(), + amount: transfer, + }; + let info = mock_info(spender.as_ref(), &[]); + let env = mock_env(); + let res = execute(deps.as_mut(), env, info, msg).unwrap(); + assert_eq!(res.attributes[0], attr("action", "burn_from")); + + // make sure money burnt + assert_eq!( + get_balance(deps.as_ref(), owner.clone()), + start.checked_sub(transfer).unwrap() + ); + + // ensure it looks good + let allowance = query_allowance(deps.as_ref(), owner.clone(), spender.clone()).unwrap(); + let expect = AllowanceResponse { + allowance: allow1.checked_sub(transfer).unwrap(), + expires: Expiration::Never {}, + }; + assert_eq!(expect, allowance); + + // cannot burn more than the allowance + let msg = ExecuteMsg::BurnFrom { + owner: owner.clone(), + amount: Uint128::from(33443u128), + }; + let info = mock_info(spender.as_ref(), &[]); + let env = mock_env(); + let err = execute(deps.as_mut(), env, info, msg).unwrap_err(); + assert!(matches!(err, ContractError::Std(StdError::Overflow { .. }))); + + // let us increase limit, but set the expiration (default env height is 12_345) + let info = mock_info(owner.as_ref(), &[]); + let env = mock_env(); + let msg = ExecuteMsg::IncreaseAllowance { + spender: spender.clone(), + amount: Uint128::from(1000u128), + expires: Some(Expiration::AtHeight(env.block.height)), + }; + execute(deps.as_mut(), env, info, msg).unwrap(); + + // we should now get the expiration error + let msg = ExecuteMsg::BurnFrom { + owner, + amount: Uint128::from(33443u128), + }; + let info = mock_info(spender.as_ref(), &[]); + let env = mock_env(); + let err = execute(deps.as_mut(), env, info, msg).unwrap_err(); + assert_eq!(err, ContractError::Expired {}); + } + + #[test] + fn send_from_respects_limits() { + let mut deps = mock_dependencies_with_balance(&[]); + let owner = String::from("addr0001"); + let spender = String::from("addr0002"); + let contract = String::from("cool-dex"); + let send_msg = Binary::from(r#"{"some":123}"#.as_bytes()); + + let start = Uint128::from(999999u128); + do_instantiate(deps.as_mut(), &owner, start); + + // provide an allowance + let allow1 = Uint128::from(77777u128); + let msg = ExecuteMsg::IncreaseAllowance { + spender: spender.clone(), + amount: allow1, + expires: None, + }; + let info = mock_info(owner.as_ref(), &[]); + let env = mock_env(); + execute(deps.as_mut(), env, info, msg).unwrap(); + + // valid send of part of the allowance + let transfer = Uint128::from(44444u128); + let msg = ExecuteMsg::SendFrom { + owner: owner.clone(), + amount: transfer, + contract: contract.clone(), + msg: send_msg.clone(), + }; + let info = mock_info(spender.as_ref(), &[]); + let env = mock_env(); + let res = execute(deps.as_mut(), env, info, msg).unwrap(); + assert_eq!(res.attributes[0], attr("action", "send_from")); + assert_eq!(1, res.messages.len()); + + // we record this as sent by the one who requested, not the one who was paying + let binary_msg = Cw20ReceiveMsg { + sender: spender.clone(), + amount: transfer, + msg: send_msg.clone(), + } + .into_binary() + .unwrap(); + assert_eq!( + res.messages[0], + SubMsg::new(CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: contract.clone(), + msg: binary_msg, + funds: vec![], + })) + ); + + // make sure money sent + assert_eq!( + get_balance(deps.as_ref(), owner.clone()), + start.checked_sub(transfer).unwrap() + ); + assert_eq!(get_balance(deps.as_ref(), contract.clone()), transfer); + + // ensure it looks good + let allowance = query_allowance(deps.as_ref(), owner.clone(), spender.clone()).unwrap(); + let expect = AllowanceResponse { + allowance: allow1.checked_sub(transfer).unwrap(), + expires: Expiration::Never {}, + }; + assert_eq!(expect, allowance); + + // cannot send more than the allowance + let msg = ExecuteMsg::SendFrom { + owner: owner.clone(), + amount: Uint128::from(33443u128), + contract: contract.clone(), + msg: send_msg.clone(), + }; + let info = mock_info(spender.as_ref(), &[]); + let env = mock_env(); + let err = execute(deps.as_mut(), env, info, msg).unwrap_err(); + assert!(matches!(err, ContractError::Std(StdError::Overflow { .. }))); + + // let us increase limit, but set the expiration to current block (expired) + let info = mock_info(owner.as_ref(), &[]); + let env = mock_env(); + let msg = ExecuteMsg::IncreaseAllowance { + spender: spender.clone(), + amount: Uint128::from(1000u128), + expires: Some(Expiration::AtHeight(env.block.height)), + }; + execute(deps.as_mut(), env, info, msg).unwrap(); + + // we should now get the expiration error + let msg = ExecuteMsg::SendFrom { + owner, + amount: Uint128::from(33443u128), + contract, + msg: send_msg, + }; + let info = mock_info(spender.as_ref(), &[]); + let env = mock_env(); + let err = execute(deps.as_mut(), env, info, msg).unwrap_err(); + assert_eq!(err, ContractError::Expired {}); + } +} diff --git a/wormhole/cw20-legacy/src/contract.rs b/wormhole/cw20-legacy/src/contract.rs new file mode 100644 index 00000000..69164721 --- /dev/null +++ b/wormhole/cw20-legacy/src/contract.rs @@ -0,0 +1,857 @@ +#[cfg(not(feature = "library"))] +use cosmwasm_std::entry_point; +use cosmwasm_std::{ + attr, to_binary, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdError, StdResult, + Uint128, +}; + +use cw2::set_contract_version; +use cw20::{BalanceResponse, Cw20Coin, Cw20ReceiveMsg, MinterResponse, TokenInfoResponse}; + +use crate::allowances::{ + execute_burn_from, execute_decrease_allowance, execute_increase_allowance, execute_send_from, + execute_transfer_from, query_allowance, +}; +use crate::enumerable::{query_all_accounts, query_all_allowances}; +use crate::error::ContractError; +use crate::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; +use crate::state::{MinterData, TokenInfo, BALANCES, TOKEN_INFO}; + +// version info for migration info +const CONTRACT_NAME: &str = "crates.io:cw20-base"; +const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn instantiate( + mut deps: DepsMut, + _env: Env, + _info: MessageInfo, + msg: InstantiateMsg, +) -> StdResult { + set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + // check valid token info + msg.validate()?; + // create initial accounts + let total_supply = create_accounts(&mut deps, &msg.initial_balances)?; + + if let Some(limit) = msg.get_cap() { + if total_supply > limit { + return Err(StdError::generic_err("Initial supply greater than cap")); + } + } + + let mint = match msg.mint { + Some(m) => Some(MinterData { + minter: deps.api.addr_canonicalize(&m.minter)?, + cap: m.cap, + }), + None => None, + }; + + // store token info + let data = TokenInfo { + name: msg.name, + symbol: msg.symbol, + decimals: msg.decimals, + total_supply, + mint, + }; + TOKEN_INFO.save(deps.storage, &data)?; + Ok(Response::default()) +} + +pub fn create_accounts(deps: &mut DepsMut, accounts: &[Cw20Coin]) -> StdResult { + let mut total_supply = Uint128::zero(); + for row in accounts { + let address = deps.api.addr_canonicalize(&row.address)?; + BALANCES.save(deps.storage, address.as_slice(), &row.amount)?; + total_supply += row.amount; + } + Ok(total_supply) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn execute( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: ExecuteMsg, +) -> Result { + match msg { + ExecuteMsg::Transfer { recipient, amount } => { + execute_transfer(deps, env, info, recipient, amount) + } + ExecuteMsg::Burn { amount } => execute_burn(deps, env, info, amount), + ExecuteMsg::Send { + contract, + amount, + msg, + } => execute_send(deps, env, info, contract, amount, msg), + ExecuteMsg::Mint { recipient, amount } => execute_mint(deps, env, info, recipient, amount), + ExecuteMsg::IncreaseAllowance { + spender, + amount, + expires, + } => execute_increase_allowance(deps, env, info, spender, amount, expires), + ExecuteMsg::DecreaseAllowance { + spender, + amount, + expires, + } => execute_decrease_allowance(deps, env, info, spender, amount, expires), + ExecuteMsg::TransferFrom { + owner, + recipient, + amount, + } => execute_transfer_from(deps, env, info, owner, recipient, amount), + ExecuteMsg::BurnFrom { owner, amount } => execute_burn_from(deps, env, info, owner, amount), + ExecuteMsg::SendFrom { + owner, + contract, + amount, + msg, + } => execute_send_from(deps, env, info, owner, contract, amount, msg), + } +} + +pub fn execute_transfer( + deps: DepsMut, + _env: Env, + info: MessageInfo, + recipient: String, + amount: Uint128, +) -> Result { + if amount == Uint128::zero() { + return Err(ContractError::InvalidZeroAmount {}); + } + + let rcpt_addr = deps.api.addr_validate(&recipient)?; + + BALANCES.update( + deps.storage, + deps.api + .addr_canonicalize(&info.sender.to_string())? + .as_slice(), + |balance: Option| -> StdResult<_> { + Ok(balance.unwrap_or_default().checked_sub(amount)?) + }, + )?; + BALANCES.update( + deps.storage, + deps.api + .addr_canonicalize(&rcpt_addr.to_string())? + .as_slice(), + |balance: Option| -> StdResult<_> { Ok(balance.unwrap_or_default() + amount) }, + )?; + + Ok(Response::new().add_attributes(vec![ + attr("action", "transfer"), + attr("from", info.sender), + attr("to", recipient), + attr("amount", amount), + ])) +} + +pub fn execute_burn( + deps: DepsMut, + _env: Env, + info: MessageInfo, + amount: Uint128, +) -> Result { + if amount == Uint128::zero() { + return Err(ContractError::InvalidZeroAmount {}); + } + + // lower balance + BALANCES.update( + deps.storage, + deps.api + .addr_canonicalize(&info.sender.to_string())? + .as_slice(), + |balance: Option| -> StdResult<_> { + Ok(balance.unwrap_or_default().checked_sub(amount)?) + }, + )?; + // reduce total_supply + TOKEN_INFO.update(deps.storage, |mut info| -> StdResult<_> { + info.total_supply = info.total_supply.checked_sub(amount)?; + Ok(info) + })?; + + Ok(Response::new().add_attributes(vec![ + attr("action", "burn"), + attr("from", info.sender), + attr("amount", amount), + ])) +} + +pub fn execute_mint( + deps: DepsMut, + _env: Env, + info: MessageInfo, + recipient: String, + amount: Uint128, +) -> Result { + if amount == Uint128::zero() { + return Err(ContractError::InvalidZeroAmount {}); + } + + let mut config = TOKEN_INFO.load(deps.storage)?; + if config.mint.is_none() + || config.mint.as_ref().unwrap().minter + != deps.api.addr_canonicalize(&info.sender.to_string())? + { + return Err(ContractError::Unauthorized {}); + } + + // update supply and enforce cap + config.total_supply += amount; + if let Some(limit) = config.get_cap() { + if config.total_supply > limit { + return Err(ContractError::CannotExceedCap {}); + } + } + TOKEN_INFO.save(deps.storage, &config)?; + + // add amount to recipient balance + let rcpt_addr = deps.api.addr_validate(&recipient)?; + BALANCES.update( + deps.storage, + deps.api + .addr_canonicalize(&rcpt_addr.to_string())? + .as_slice(), + |balance: Option| -> StdResult<_> { Ok(balance.unwrap_or_default() + amount) }, + )?; + + Ok(Response::new().add_attributes(vec![ + attr("action", "mint"), + attr("to", recipient), + attr("amount", amount), + ])) +} + +pub fn execute_send( + deps: DepsMut, + _env: Env, + info: MessageInfo, + contract: String, + amount: Uint128, + msg: Binary, +) -> Result { + if amount == Uint128::zero() { + return Err(ContractError::InvalidZeroAmount {}); + } + + let rcpt_addr = deps.api.addr_validate(&contract)?; + + // move the tokens to the contract + BALANCES.update( + deps.storage, + deps.api + .addr_canonicalize(&info.sender.to_string())? + .as_slice(), + |balance: Option| -> StdResult<_> { + Ok(balance.unwrap_or_default().checked_sub(amount)?) + }, + )?; + BALANCES.update( + deps.storage, + deps.api + .addr_canonicalize(&rcpt_addr.to_string())? + .as_slice(), + |balance: Option| -> StdResult<_> { Ok(balance.unwrap_or_default() + amount) }, + )?; + + let attrs = vec![ + attr("action", "send"), + attr("from", &info.sender), + attr("to", &contract), + attr("amount", amount), + ]; + + Ok(Response::new().add_attributes(attrs).add_message( + Cw20ReceiveMsg { + sender: info.sender.into(), + amount, + msg, + } + .into_cosmos_msg(contract)?, + )) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { + match msg { + QueryMsg::Balance { address } => to_binary(&query_balance(deps, address)?), + QueryMsg::TokenInfo {} => to_binary(&query_token_info(deps)?), + QueryMsg::Minter {} => to_binary(&query_minter(deps)?), + QueryMsg::Allowance { owner, spender } => { + to_binary(&query_allowance(deps, owner, spender)?) + } + QueryMsg::AllAllowances { + owner, + start_after, + limit, + } => to_binary(&query_all_allowances(deps, owner, start_after, limit)?), + QueryMsg::AllAccounts { start_after, limit } => { + to_binary(&query_all_accounts(deps, start_after, limit)?) + } + } +} + +pub fn query_balance(deps: Deps, address: String) -> StdResult { + let address = deps.api.addr_canonicalize(&address)?; + let balance = BALANCES + .may_load(deps.storage, address.as_slice())? + .unwrap_or_default(); + Ok(BalanceResponse { balance }) +} + +pub fn query_token_info(deps: Deps) -> StdResult { + let info = TOKEN_INFO.load(deps.storage)?; + let res = TokenInfoResponse { + name: info.name, + symbol: info.symbol, + decimals: info.decimals, + total_supply: info.total_supply, + }; + Ok(res) +} + +pub fn query_minter(deps: Deps) -> StdResult> { + let meta = TOKEN_INFO.load(deps.storage)?; + let minter = match meta.mint { + Some(m) => Some(MinterResponse { + minter: deps.api.addr_humanize(&m.minter)?.into(), + cap: m.cap, + }), + None => None, + }; + Ok(minter) +} + +#[cfg(test)] +mod tests { + use cosmwasm_std::testing::{ + mock_dependencies, mock_dependencies_with_balance, mock_env, mock_info, + }; + use cosmwasm_std::{coins, from_binary, CosmosMsg, StdError, SubMsg, WasmMsg}; + + use super::*; + + fn get_balance>(deps: Deps, address: T) -> Uint128 { + query_balance(deps, address.into()).unwrap().balance + } + + // this will set up the instantiation for other tests + fn do_instantiate_with_minter( + deps: DepsMut, + addr: &str, + amount: Uint128, + minter: &str, + cap: Option, + ) -> TokenInfoResponse { + _do_instantiate( + deps, + addr, + amount, + Some(MinterResponse { + minter: minter.to_string(), + cap, + }), + ) + } + + // this will set up the instantiation for other tests + fn do_instantiate(deps: DepsMut, addr: &str, amount: Uint128) -> TokenInfoResponse { + _do_instantiate(deps, addr, amount, None) + } + + // this will set up the instantiation for other tests + fn _do_instantiate( + mut deps: DepsMut, + addr: &str, + amount: Uint128, + mint: Option, + ) -> TokenInfoResponse { + let instantiate_msg = InstantiateMsg { + name: "Auto Gen".to_string(), + symbol: "AUTO".to_string(), + decimals: 3, + initial_balances: vec![Cw20Coin { + address: addr.to_string(), + amount, + }], + mint: mint.clone(), + }; + let info = mock_info("creator", &[]); + let env = mock_env(); + let res = instantiate(deps.branch(), env, info, instantiate_msg).unwrap(); + assert_eq!(0, res.messages.len()); + + let meta = query_token_info(deps.as_ref()).unwrap(); + assert_eq!( + meta, + TokenInfoResponse { + name: "Auto Gen".to_string(), + symbol: "AUTO".to_string(), + decimals: 3, + total_supply: amount, + } + ); + assert_eq!(get_balance(deps.as_ref(), addr), amount); + assert_eq!(query_minter(deps.as_ref()).unwrap(), mint,); + meta + } + + #[test] + fn proper_instantiation() { + let mut deps = mock_dependencies(); + let amount = Uint128::from(11223344u128); + let instantiate_msg = InstantiateMsg { + name: "Cash Token".to_string(), + symbol: "CASH".to_string(), + decimals: 9, + initial_balances: vec![Cw20Coin { + address: String::from("addr0000"), + amount, + }], + mint: None, + }; + let info = mock_info("creator", &[]); + let env = mock_env(); + let res = instantiate(deps.as_mut(), env, info, instantiate_msg).unwrap(); + assert_eq!(0, res.messages.len()); + + assert_eq!( + query_token_info(deps.as_ref()).unwrap(), + TokenInfoResponse { + name: "Cash Token".to_string(), + symbol: "CASH".to_string(), + decimals: 9, + total_supply: amount, + } + ); + assert_eq!( + get_balance(deps.as_ref(), "addr0000"), + Uint128::from(11223344u128) + ); + } + + #[test] + fn instantiate_mintable() { + let mut deps = mock_dependencies(); + let amount = Uint128::from(11223344u128); + let minter = String::from("asmodat"); + let limit = Uint128::from(511223344u128); + let instantiate_msg = InstantiateMsg { + name: "Cash Token".to_string(), + symbol: "CASH".to_string(), + decimals: 9, + initial_balances: vec![Cw20Coin { + address: "addr0000".into(), + amount, + }], + mint: Some(MinterResponse { + minter: minter.clone(), + cap: Some(limit), + }), + }; + let info = mock_info("creator", &[]); + let env = mock_env(); + let res = instantiate(deps.as_mut(), env, info, instantiate_msg).unwrap(); + assert_eq!(0, res.messages.len()); + + assert_eq!( + query_token_info(deps.as_ref()).unwrap(), + TokenInfoResponse { + name: "Cash Token".to_string(), + symbol: "CASH".to_string(), + decimals: 9, + total_supply: amount, + } + ); + assert_eq!( + get_balance(deps.as_ref(), "addr0000"), + Uint128::from(11223344u128) + ); + assert_eq!( + query_minter(deps.as_ref()).unwrap(), + Some(MinterResponse { + minter, + cap: Some(limit), + }), + ); + } + + #[test] + fn instantiate_mintable_over_cap() { + let mut deps = mock_dependencies(); + let amount = Uint128::from(11223344u128); + let minter = String::from("asmodat"); + let limit = Uint128::from(11223300u128); + let instantiate_msg = InstantiateMsg { + name: "Cash Token".to_string(), + symbol: "CASH".to_string(), + decimals: 9, + initial_balances: vec![Cw20Coin { + address: String::from("addr0000"), + amount, + }], + mint: Some(MinterResponse { + minter, + cap: Some(limit), + }), + }; + let info = mock_info("creator", &[]); + let env = mock_env(); + let err = instantiate(deps.as_mut(), env, info, instantiate_msg).unwrap_err(); + assert_eq!( + err, + StdError::generic_err("Initial supply greater than cap") + ); + } + + #[test] + fn can_mint_by_minter() { + let mut deps = mock_dependencies(); + + let genesis = String::from("genesis"); + let amount = Uint128::from(11223344u128); + let minter = String::from("asmodat"); + let limit = Uint128::from(511223344u128); + do_instantiate_with_minter(deps.as_mut(), &genesis, amount, &minter, Some(limit)); + + // minter can mint coins to some winner + let winner = String::from("lucky"); + let prize = Uint128::new(222_222_222); + let msg = ExecuteMsg::Mint { + recipient: winner.clone(), + amount: prize, + }; + + let info = mock_info(minter.as_ref(), &[]); + let env = mock_env(); + let res = execute(deps.as_mut(), env, info, msg).unwrap(); + assert_eq!(0, res.messages.len()); + assert_eq!(get_balance(deps.as_ref(), genesis), amount); + assert_eq!(get_balance(deps.as_ref(), winner.clone()), prize); + + // but cannot mint nothing + let msg = ExecuteMsg::Mint { + recipient: winner.clone(), + amount: Uint128::zero(), + }; + let info = mock_info(minter.as_ref(), &[]); + let env = mock_env(); + let err = execute(deps.as_mut(), env, info, msg).unwrap_err(); + assert_eq!(err, ContractError::InvalidZeroAmount {}); + + // but if it exceeds cap (even over multiple rounds), it fails + // cap is enforced + let msg = ExecuteMsg::Mint { + recipient: winner, + amount: Uint128::new(333_222_222), + }; + let info = mock_info(minter.as_ref(), &[]); + let env = mock_env(); + let err = execute(deps.as_mut(), env, info, msg).unwrap_err(); + assert_eq!(err, ContractError::CannotExceedCap {}); + } + + #[test] + fn others_cannot_mint() { + let mut deps = mock_dependencies(); + do_instantiate_with_minter( + deps.as_mut(), + &String::from("genesis"), + Uint128::from(1234u128), + &String::from("minter"), + None, + ); + + let msg = ExecuteMsg::Mint { + recipient: String::from("lucky"), + amount: Uint128::new(222), + }; + let info = mock_info("anyone else", &[]); + let env = mock_env(); + let err = execute(deps.as_mut(), env, info, msg).unwrap_err(); + assert_eq!(err, ContractError::Unauthorized {}); + } + + #[test] + fn no_one_mints_if_minter_unset() { + let mut deps = mock_dependencies(); + do_instantiate(deps.as_mut(), &String::from("genesis"), Uint128::new(1234)); + + let msg = ExecuteMsg::Mint { + recipient: String::from("lucky"), + amount: Uint128::new(222), + }; + let info = mock_info("genesis", &[]); + let env = mock_env(); + let err = execute(deps.as_mut(), env, info, msg).unwrap_err(); + assert_eq!(err, ContractError::Unauthorized {}); + } + + #[test] + fn instantiate_multiple_accounts() { + let mut deps = mock_dependencies(); + let amount1 = Uint128::from(11223344u128); + let addr1 = String::from("addr0001"); + let amount2 = Uint128::from(7890987u128); + let addr2 = String::from("addr0002"); + let instantiate_msg = InstantiateMsg { + name: "Bash Shell".to_string(), + symbol: "BASH".to_string(), + decimals: 6, + initial_balances: vec![ + Cw20Coin { + address: addr1.clone(), + amount: amount1, + }, + Cw20Coin { + address: addr2.clone(), + amount: amount2, + }, + ], + mint: None, + }; + let info = mock_info("creator", &[]); + let env = mock_env(); + let res = instantiate(deps.as_mut(), env, info, instantiate_msg).unwrap(); + assert_eq!(0, res.messages.len()); + + assert_eq!( + query_token_info(deps.as_ref()).unwrap(), + TokenInfoResponse { + name: "Bash Shell".to_string(), + symbol: "BASH".to_string(), + decimals: 6, + total_supply: amount1 + amount2, + } + ); + assert_eq!(get_balance(deps.as_ref(), addr1), amount1); + assert_eq!(get_balance(deps.as_ref(), addr2), amount2); + } + + #[test] + fn queries_work() { + let mut deps = mock_dependencies_with_balance(&coins(2, "token")); + let addr1 = String::from("addr0001"); + let amount1 = Uint128::from(12340000u128); + + let expected = do_instantiate(deps.as_mut(), &addr1, amount1); + + // check meta query + let loaded = query_token_info(deps.as_ref()).unwrap(); + assert_eq!(expected, loaded); + + let _info = mock_info("test", &[]); + let env = mock_env(); + // check balance query (full) + let data = query( + deps.as_ref(), + env.clone(), + QueryMsg::Balance { address: addr1 }, + ) + .unwrap(); + let loaded: BalanceResponse = from_binary(&data).unwrap(); + assert_eq!(loaded.balance, amount1); + + // check balance query (empty) + let data = query( + deps.as_ref(), + env, + QueryMsg::Balance { + address: String::from("addr0002"), + }, + ) + .unwrap(); + let loaded: BalanceResponse = from_binary(&data).unwrap(); + assert_eq!(loaded.balance, Uint128::zero()); + } + + #[test] + fn transfer() { + let mut deps = mock_dependencies_with_balance(&coins(2, "token")); + let addr1 = String::from("addr0001"); + let addr2 = String::from("addr0002"); + let amount1 = Uint128::from(12340000u128); + let transfer = Uint128::from(76543u128); + let too_much = Uint128::from(12340321u128); + + do_instantiate(deps.as_mut(), &addr1, amount1); + + // cannot transfer nothing + let info = mock_info(addr1.as_ref(), &[]); + let env = mock_env(); + let msg = ExecuteMsg::Transfer { + recipient: addr2.clone(), + amount: Uint128::zero(), + }; + let err = execute(deps.as_mut(), env, info, msg).unwrap_err(); + assert_eq!(err, ContractError::InvalidZeroAmount {}); + + // cannot send more than we have + let info = mock_info(addr1.as_ref(), &[]); + let env = mock_env(); + let msg = ExecuteMsg::Transfer { + recipient: addr2.clone(), + amount: too_much, + }; + let err = execute(deps.as_mut(), env, info, msg).unwrap_err(); + assert!(matches!(err, ContractError::Std(StdError::Overflow { .. }))); + + // cannot send from empty account + let info = mock_info(addr2.as_ref(), &[]); + let env = mock_env(); + let msg = ExecuteMsg::Transfer { + recipient: addr1.clone(), + amount: transfer, + }; + let err = execute(deps.as_mut(), env, info, msg).unwrap_err(); + assert!(matches!(err, ContractError::Std(StdError::Overflow { .. }))); + + // valid transfer + let info = mock_info(addr1.as_ref(), &[]); + let env = mock_env(); + let msg = ExecuteMsg::Transfer { + recipient: addr2.clone(), + amount: transfer, + }; + let res = execute(deps.as_mut(), env, info, msg).unwrap(); + assert_eq!(res.messages.len(), 0); + + let remainder = amount1.checked_sub(transfer).unwrap(); + assert_eq!(get_balance(deps.as_ref(), addr1), remainder); + assert_eq!(get_balance(deps.as_ref(), addr2), transfer); + assert_eq!( + query_token_info(deps.as_ref()).unwrap().total_supply, + amount1 + ); + } + + #[test] + fn burn() { + let mut deps = mock_dependencies_with_balance(&coins(2, "token")); + let addr1 = String::from("addr0001"); + let amount1 = Uint128::from(12340000u128); + let burn = Uint128::from(76543u128); + let too_much = Uint128::from(12340321u128); + + do_instantiate(deps.as_mut(), &addr1, amount1); + + // cannot burn nothing + let info = mock_info(addr1.as_ref(), &[]); + let env = mock_env(); + let msg = ExecuteMsg::Burn { + amount: Uint128::zero(), + }; + let err = execute(deps.as_mut(), env, info, msg).unwrap_err(); + assert_eq!(err, ContractError::InvalidZeroAmount {}); + assert_eq!( + query_token_info(deps.as_ref()).unwrap().total_supply, + amount1 + ); + + // cannot burn more than we have + let info = mock_info(addr1.as_ref(), &[]); + let env = mock_env(); + let msg = ExecuteMsg::Burn { amount: too_much }; + let err = execute(deps.as_mut(), env, info, msg).unwrap_err(); + assert!(matches!(err, ContractError::Std(StdError::Overflow { .. }))); + assert_eq!( + query_token_info(deps.as_ref()).unwrap().total_supply, + amount1 + ); + + // valid burn reduces total supply + let info = mock_info(addr1.as_ref(), &[]); + let env = mock_env(); + let msg = ExecuteMsg::Burn { amount: burn }; + let res = execute(deps.as_mut(), env, info, msg).unwrap(); + assert_eq!(res.messages.len(), 0); + + let remainder = amount1.checked_sub(burn).unwrap(); + assert_eq!(get_balance(deps.as_ref(), addr1), remainder); + assert_eq!( + query_token_info(deps.as_ref()).unwrap().total_supply, + remainder + ); + } + + #[test] + fn send() { + let mut deps = mock_dependencies_with_balance(&coins(2, "token")); + let addr1 = String::from("addr0001"); + let contract = String::from("addr0002"); + let amount1 = Uint128::from(12340000u128); + let transfer = Uint128::from(76543u128); + let too_much = Uint128::from(12340321u128); + let send_msg = Binary::from(r#"{"some":123}"#.as_bytes()); + + do_instantiate(deps.as_mut(), &addr1, amount1); + + // cannot send nothing + let info = mock_info(addr1.as_ref(), &[]); + let env = mock_env(); + let msg = ExecuteMsg::Send { + contract: contract.clone(), + amount: Uint128::zero(), + msg: send_msg.clone(), + }; + let err = execute(deps.as_mut(), env, info, msg).unwrap_err(); + assert_eq!(err, ContractError::InvalidZeroAmount {}); + + // cannot send more than we have + let info = mock_info(addr1.as_ref(), &[]); + let env = mock_env(); + let msg = ExecuteMsg::Send { + contract: contract.clone(), + amount: too_much, + msg: send_msg.clone(), + }; + let err = execute(deps.as_mut(), env, info, msg).unwrap_err(); + assert!(matches!(err, ContractError::Std(StdError::Overflow { .. }))); + + // valid transfer + let info = mock_info(addr1.as_ref(), &[]); + let env = mock_env(); + let msg = ExecuteMsg::Send { + contract: contract.clone(), + amount: transfer, + msg: send_msg.clone(), + }; + let res = execute(deps.as_mut(), env, info, msg).unwrap(); + assert_eq!(res.messages.len(), 1); + + // ensure proper send message sent + // this is the message we want delivered to the other side + let binary_msg = Cw20ReceiveMsg { + sender: addr1.clone(), + amount: transfer, + msg: send_msg, + } + .into_binary() + .unwrap(); + // and this is how it must be wrapped for the vm to process it + assert_eq!( + res.messages[0], + SubMsg::new(CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: contract.clone(), + msg: binary_msg, + funds: vec![], + })) + ); + + // ensure balance is properly transferred + let remainder = amount1.checked_sub(transfer).unwrap(); + assert_eq!(get_balance(deps.as_ref(), addr1), remainder); + assert_eq!(get_balance(deps.as_ref(), contract), transfer); + assert_eq!( + query_token_info(deps.as_ref()).unwrap().total_supply, + amount1 + ); + } +} diff --git a/wormhole/cw20-legacy/src/enumerable.rs b/wormhole/cw20-legacy/src/enumerable.rs new file mode 100644 index 00000000..32b70a04 --- /dev/null +++ b/wormhole/cw20-legacy/src/enumerable.rs @@ -0,0 +1,449 @@ +use cosmwasm_std::{Addr, Api, CanonicalAddr, Deps, Order, StdResult}; +use cw20::{AllAccountsResponse, AllAllowancesResponse, AllowanceInfo}; + +use crate::state::{ALLOWANCES, BALANCES}; +use cw_storage_plus::Bound; + +// settings for pagination +const MAX_LIMIT: u32 = 30; +const DEFAULT_LIMIT: u32 = 10; + +pub fn calc_range_start_human( + api: &dyn Api, + start_after: Option, +) -> StdResult>> { + match start_after { + Some(human) => { + let mut v: Vec = api.addr_canonicalize(human.as_ref())?.0.into(); + v.push(0); + Ok(Some(v)) + } + None => Ok(None), + } +} + +pub fn query_all_allowances( + deps: Deps, + owner: String, + start_after: Option, + limit: Option, +) -> StdResult { + let owner_addr = deps.api.addr_canonicalize(&owner)?; + let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize; + let start = calc_range_start_human(deps.api, start_after.map(Addr::unchecked))?; + let start = start.as_deref().map(Bound::exclusive); + + let allowances: StdResult> = ALLOWANCES + .prefix(owner_addr.as_slice()) + .range(deps.storage, start, None, Order::Ascending) + .take(limit) + .map(|item| { + let (k, v) = item?; + Ok(AllowanceInfo { + spender: deps.api.addr_humanize(&CanonicalAddr::from(k))?.to_string(), + allowance: v.allowance, + expires: v.expires, + }) + }) + .collect(); + Ok(AllAllowancesResponse { + allowances: allowances?, + }) +} + +pub fn query_all_accounts( + deps: Deps, + start_after: Option, + limit: Option, +) -> StdResult { + let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize; + let start = calc_range_start_human(deps.api, start_after.map(Addr::unchecked))?; + let start = start.as_deref().map(Bound::exclusive); + + let accounts: Result, _> = BALANCES + .keys(deps.storage, start, None, Order::Ascending) + .map(|k| { + deps.api + .addr_humanize(&CanonicalAddr::from(k?)) + .map(|v| v.to_string()) + }) + .take(limit) + .collect(); + + Ok(AllAccountsResponse { + accounts: accounts?, + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + use cosmwasm_std::testing::{mock_dependencies_with_balance, mock_env, mock_info}; + use cosmwasm_std::Api; + use cosmwasm_std::{coins, DepsMut, Uint128}; + use cw20::{Cw20Coin, Expiration, TokenInfoResponse}; + + use crate::contract::{execute, instantiate, query_token_info}; + use crate::msg::{ExecuteMsg, InstantiateMsg}; + + // this will set up the instantiation for other tests + fn do_instantiate(mut deps: DepsMut, addr: &str, amount: Uint128) -> TokenInfoResponse { + let instantiate_msg = InstantiateMsg { + name: "Auto Gen".to_string(), + symbol: "AUTO".to_string(), + decimals: 3, + initial_balances: vec![Cw20Coin { + address: addr.into(), + amount, + }], + mint: None, + }; + let info = mock_info("creator", &[]); + let env = mock_env(); + instantiate(deps.branch(), env, info, instantiate_msg).unwrap(); + query_token_info(deps.as_ref()).unwrap() + } + + #[test] + fn query_all_allowances_works() { + let mut deps = mock_dependencies_with_balance(&coins(2, "token")); + + let owner = String::from("owner"); + // these are in alphabetical order same than insert order + + let spender1 = deps + .api + .addr_humanize(&CanonicalAddr::from(vec![ + 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ])) + .unwrap(); + let spender2 = deps + .api + .addr_humanize(&CanonicalAddr::from(vec![ + 1, 1, 1, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ])) + .unwrap(); + + let info = mock_info(owner.as_ref(), &[]); + let env = mock_env(); + do_instantiate(deps.as_mut(), &owner, Uint128::from(12340000u128)); + + // no allowance to start + let allowances = query_all_allowances(deps.as_ref(), owner.clone(), None, None).unwrap(); + assert_eq!(allowances.allowances, vec![]); + + // set allowance with height expiration + let allow1 = Uint128::from(7777u128); + let expires = Expiration::AtHeight(5432); + let msg = ExecuteMsg::IncreaseAllowance { + spender: spender1.to_string(), + amount: allow1, + expires: Some(expires), + }; + execute(deps.as_mut(), env.clone(), info.clone(), msg).unwrap(); + + // set allowance with no expiration + let allow2 = Uint128::from(54321u128); + let msg = ExecuteMsg::IncreaseAllowance { + spender: spender2.to_string(), + amount: allow2, + expires: None, + }; + execute(deps.as_mut(), env, info, msg).unwrap(); + + // query list gets 2 + let allowances = query_all_allowances(deps.as_ref(), owner.clone(), None, None).unwrap(); + assert_eq!(allowances.allowances.len(), 2); + + // first one is spender1 (order of CanonicalAddr uncorrelated with String) + let allowances = query_all_allowances(deps.as_ref(), owner.clone(), None, Some(1)).unwrap(); + assert_eq!(allowances.allowances.len(), 1); + let allow = &allowances.allowances[0]; + assert_eq!(&allow.spender, &spender1); + assert_eq!(&allow.expires, &expires); + assert_eq!(&allow.allowance, &allow1); + + // next one is spender2 + let allowances = query_all_allowances( + deps.as_ref(), + owner, + Some(allow.spender.clone()), + Some(10000), + ) + .unwrap(); + assert_eq!(allowances.allowances.len(), 1); + let allow = &allowances.allowances[0]; + assert_eq!(&allow.spender, &spender2); + assert_eq!(&allow.expires, &Expiration::Never {}); + assert_eq!(&allow.allowance, &allow2); + } + + #[test] + fn query_all_accounts_works() { + let mut deps = mock_dependencies_with_balance(&coins(2, "token")); + + // insert order and lexicographical order are different + let acct1 = deps + .api + .addr_humanize(&CanonicalAddr::from(vec![ + 1, 1, 1, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ])) + .unwrap() + .to_string(); + let acct2 = deps + .api + .addr_humanize(&CanonicalAddr::from(vec![ + 1, 1, 1, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ])) + .unwrap() + .to_string(); + let acct3 = deps + .api + .addr_humanize(&CanonicalAddr::from(vec![ + 1, 1, 1, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ])) + .unwrap() + .to_string(); + let acct4 = deps + .api + .addr_humanize(&CanonicalAddr::from(vec![ + 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ])) + .unwrap() + .to_string(); + let expected_order = [acct4.clone(), acct1.clone(), acct3.clone(), acct2.clone()]; + + do_instantiate(deps.as_mut(), &acct1, Uint128::from(12340000u128)); + + // put money everywhere (to create balances) + let info = mock_info(acct1.as_ref(), &[]); + let env = mock_env(); + execute( + deps.as_mut(), + env.clone(), + info.clone(), + ExecuteMsg::Transfer { + recipient: acct2, + amount: Uint128::from(222222u128), + }, + ) + .unwrap(); + execute( + deps.as_mut(), + env.clone(), + info.clone(), + ExecuteMsg::Transfer { + recipient: acct3, + amount: Uint128::from(333333u128), + }, + ) + .unwrap(); + execute( + deps.as_mut(), + env, + info, + ExecuteMsg::Transfer { + recipient: acct4, + amount: Uint128::from(444444u128), + }, + ) + .unwrap(); + + // make sure we get the proper results + let accounts = query_all_accounts(deps.as_ref(), None, None).unwrap(); + assert_eq!(accounts.accounts, expected_order); + + // let's do pagination + let accounts = query_all_accounts(deps.as_ref(), None, Some(2)).unwrap(); + assert_eq!(accounts.accounts, expected_order[0..2].to_vec()); + + let accounts = + query_all_accounts(deps.as_ref(), Some(accounts.accounts[1].clone()), Some(1)).unwrap(); + assert_eq!(accounts.accounts, expected_order[2..3].to_vec()); + + let accounts = + query_all_accounts(deps.as_ref(), Some(accounts.accounts[0].clone()), Some(777)) + .unwrap(); + assert_eq!(accounts.accounts, expected_order[3..].to_vec()); + } + + use cosmwasm_std::{StdResult, Storage}; + use cosmwasm_storage::{bucket, bucket_read, Bucket, ReadonlyBucket, ReadonlyPrefixedStorage}; + use cw20::AllowanceResponse; + + const PREFIX_BALANCE: &[u8] = b"balance"; + const PREFIX_ALLOWANCE: &[u8] = b"allowance"; + + /// balances are state of the erc20 tokens + pub fn legacy_balances(storage: &mut dyn Storage) -> Bucket { + bucket(storage, PREFIX_BALANCE) + } + + /// balances are state of the erc20 tokens (read-only version for queries) + pub fn legacy_balances_read(storage: &dyn Storage) -> ReadonlyBucket { + bucket_read(storage, PREFIX_BALANCE) + } + + pub fn legacy_balances_prefix_read(storage: &dyn Storage) -> ReadonlyPrefixedStorage { + ReadonlyPrefixedStorage::new(storage, PREFIX_BALANCE) + } + + /// returns a bucket with all allowances authorized by this owner (query it by spender) + pub fn legacy_allowances<'a>( + storage: &'a mut dyn Storage, + owner: &CanonicalAddr, + ) -> Bucket<'a, AllowanceResponse> { + Bucket::multilevel(storage, &[PREFIX_ALLOWANCE, owner.as_slice()]) + } + + /// returns a bucket with all allowances authorized by this owner (query it by spender) + /// (read-only version for queries) + pub fn legacy_allowances_read<'a>( + storage: &'a dyn Storage, + owner: &CanonicalAddr, + ) -> ReadonlyBucket<'a, AllowanceResponse> { + ReadonlyBucket::multilevel(storage, &[PREFIX_ALLOWANCE, owner.as_slice()]) + } + + // const PREFIX_PAIR_INFO: &[u8] = b"pair_info"; + pub fn legacy_query_all_allowances( + storage: &dyn Storage, + api: &dyn Api, + owner: Addr, + start_after: Option, + limit: Option, + ) -> StdResult { + let owner_raw = api.addr_canonicalize(owner.as_str())?; + let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize; + let start = calc_range_start_human(api, start_after)?; + let allowances: StdResult> = legacy_allowances_read(storage, &owner_raw) + .range(start.as_deref(), None, Order::Ascending) + .take(limit) + .map(|item| { + let (k, v) = item?; + Ok(AllowanceInfo { + spender: api.addr_humanize(&CanonicalAddr::from(k))?.to_string(), + allowance: v.allowance, + expires: v.expires, + }) + }) + .collect(); + + Ok(AllAllowancesResponse { + allowances: allowances?, + }) + } + + pub fn legacy_query_all_accounts( + storage: &dyn Storage, + api: &dyn Api, + start_after: Option, + limit: Option, + ) -> StdResult { + let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize; + let start = calc_range_start_human(api, start_after)?; + let accounts: StdResult> = legacy_balances_prefix_read(storage) + .range(start.as_deref(), None, Order::Ascending) + .take(limit) + .map(|(k, _)| { + api.addr_humanize(&CanonicalAddr::from(k)) + .map(|v| v.to_string()) + }) + .collect(); + + Ok(AllAccountsResponse { + accounts: accounts?, + }) + } + + #[test] + fn balances_legacy_compatibility() { + let mut deps = mock_dependencies_with_balance(&[]); + let mut balances = legacy_balances(&mut deps.storage); + let addr1 = deps.api.addr_canonicalize("addr0000").unwrap(); + let addr2 = deps.api.addr_canonicalize("addr0001").unwrap(); + let key1 = addr1.as_slice(); + let key2 = addr2.as_slice(); + + balances.save(key1, &Uint128::from(100u128)).unwrap(); + balances.save(key2, &Uint128::from(200u128)).unwrap(); + + let balances_read = legacy_balances_read(&deps.storage); + assert_eq!( + BALANCES.load(&deps.storage, key1).unwrap(), + balances_read.load(key1).unwrap() + ); + + assert_eq!( + BALANCES.load(&deps.storage, key2).unwrap(), + balances_read.load(key2).unwrap() + ); + + assert_eq!( + query_all_accounts(deps.as_ref(), None, None), + legacy_query_all_accounts(&deps.storage, &deps.api, None, None), + ); + } + + #[test] + fn allowance_legacy_compatibility() { + let mut deps = mock_dependencies_with_balance(&[]); + let owner_addr = deps.api.addr_canonicalize("owner0000").unwrap(); + let owner_key = owner_addr.as_slice(); + let addr1 = deps.api.addr_canonicalize("addr0000").unwrap(); + let addr2 = deps.api.addr_canonicalize("addr0001").unwrap(); + let key1 = addr1.as_slice(); + let key2 = addr2.as_slice(); + + let mut allowances = legacy_allowances(&mut deps.storage, &owner_addr); + + allowances + .save( + key1, + &AllowanceResponse { + allowance: Uint128::from(100u128), + expires: Expiration::AtHeight(5432), + }, + ) + .unwrap(); + + allowances + .save( + key2, + &AllowanceResponse { + allowance: Uint128::from(200u128), + expires: Expiration::AtHeight(2345), + }, + ) + .unwrap(); + + let allowance_read = legacy_allowances_read(&deps.storage, &owner_addr); + assert_eq!( + ALLOWANCES.load(&deps.storage, (owner_key, key1)).unwrap(), + allowance_read.load(key1).unwrap() + ); + + assert_eq!( + ALLOWANCES.load(&deps.storage, (owner_key, key2)).unwrap(), + allowance_read.load(key2).unwrap() + ); + + assert_eq!( + query_all_allowances(deps.as_ref(), "owner0000".to_string(), None, None), + legacy_query_all_allowances( + &deps.storage, + &deps.api, + Addr::unchecked("owner0000"), + None, + None + ), + ); + } +} diff --git a/wormhole/cw20-legacy/src/error.rs b/wormhole/cw20-legacy/src/error.rs new file mode 100644 index 00000000..3a7ef0f6 --- /dev/null +++ b/wormhole/cw20-legacy/src/error.rs @@ -0,0 +1,26 @@ +use cosmwasm_std::StdError; +use thiserror::Error; + +#[derive(Error, Debug, PartialEq)] +pub enum ContractError { + #[error("{0}")] + Std(#[from] StdError), + + #[error("Unauthorized")] + Unauthorized {}, + + #[error("Cannot set to own account")] + CannotSetOwnAccount {}, + + #[error("Invalid zero amount")] + InvalidZeroAmount {}, + + #[error("Allowance is expired")] + Expired {}, + + #[error("No allowance for this account")] + NoAllowance {}, + + #[error("Minting cannot exceed the cap")] + CannotExceedCap {}, +} diff --git a/wormhole/cw20-legacy/src/lib.rs b/wormhole/cw20-legacy/src/lib.rs new file mode 100644 index 00000000..64e8019b --- /dev/null +++ b/wormhole/cw20-legacy/src/lib.rs @@ -0,0 +1,8 @@ +pub mod allowances; +pub mod contract; +pub mod enumerable; +mod error; +pub mod msg; +pub mod state; + +pub use crate::error::ContractError; diff --git a/wormhole/cw20-legacy/src/msg.rs b/wormhole/cw20-legacy/src/msg.rs new file mode 100644 index 00000000..949e2509 --- /dev/null +++ b/wormhole/cw20-legacy/src/msg.rs @@ -0,0 +1,144 @@ +use cosmwasm_std::{Binary, StdError, StdResult, Uint128}; +use cw20::{Cw20Coin, Expiration, MinterResponse}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, JsonSchema)] +pub struct InstantiateMsg { + pub name: String, + pub symbol: String, + pub decimals: u8, + pub initial_balances: Vec, + pub mint: Option, +} + +impl InstantiateMsg { + pub fn get_cap(&self) -> Option { + self.mint.as_ref().and_then(|v| v.cap) + } + + pub fn validate(&self) -> StdResult<()> { + // Check name, symbol, decimals + if !is_valid_name(&self.name) { + return Err(StdError::generic_err( + "Name is not in the expected format (3-50 UTF-8 bytes)", + )); + } + if !is_valid_symbol(&self.symbol) { + return Err(StdError::generic_err( + "Ticker symbol is not in expected format [a-zA-Z\\-]{3,12}", + )); + } + if self.decimals > 18 { + return Err(StdError::generic_err("Decimals must not exceed 18")); + } + Ok(()) + } +} + +fn is_valid_name(name: &str) -> bool { + let bytes = name.as_bytes(); + if bytes.len() < 3 || bytes.len() > 50 { + return false; + } + true +} + +fn is_valid_symbol(symbol: &str) -> bool { + let bytes = symbol.as_bytes(); + if bytes.len() < 3 || bytes.len() > 12 { + return false; + } + for byte in bytes.iter() { + if (*byte != 45) && (*byte < 65 || *byte > 90) && (*byte < 97 || *byte > 122) { + return false; + } + } + true +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum ExecuteMsg { + /// Transfer is a base message to move tokens to another account without triggering actions + Transfer { recipient: String, amount: Uint128 }, + /// Burn is a base message to destroy tokens forever + Burn { amount: Uint128 }, + /// Send is a base message to transfer tokens to a contract and trigger an action + /// on the receiving contract. + Send { + contract: String, + amount: Uint128, + msg: Binary, + }, + /// Only with the "mintable" extension. If authorized, creates amount new tokens + /// and adds to the recipient balance. + Mint { recipient: String, amount: Uint128 }, + /// Only with "approval" extension. Allows spender to access an additional amount tokens + /// from the owner's (env.sender) account. If expires is Some(), overwrites current allowance + /// expiration with this one. + IncreaseAllowance { + spender: String, + amount: Uint128, + expires: Option, + }, + /// Only with "approval" extension. Lowers the spender's access of tokens + /// from the owner's (env.sender) account by amount. If expires is Some(), overwrites current + /// allowance expiration with this one. + DecreaseAllowance { + spender: String, + amount: Uint128, + expires: Option, + }, + /// Only with "approval" extension. Transfers amount tokens from owner -> recipient + /// if `env.sender` has sufficient pre-approval. + TransferFrom { + owner: String, + recipient: String, + amount: Uint128, + }, + /// Only with "approval" extension. Sends amount tokens from owner -> contract + /// if `env.sender` has sufficient pre-approval. + SendFrom { + owner: String, + contract: String, + amount: Uint128, + msg: Binary, + }, + /// Only with "approval" extension. Destroys tokens forever + BurnFrom { owner: String, amount: Uint128 }, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum QueryMsg { + /// Returns the current balance of the given address, 0 if unset. + /// Return type: BalanceResponse. + Balance { address: String }, + /// Returns metadata on the contract - name, decimals, supply, etc. + /// Return type: TokenInfoResponse. + TokenInfo {}, + /// Only with "mintable" extension. + /// Returns who can mint and how much. + /// Return type: MinterResponse. + Minter {}, + /// Only with "allowance" extension. + /// Returns how much spender can use from owner account, 0 if unset. + /// Return type: AllowanceResponse. + Allowance { owner: String, spender: String }, + /// Only with "enumerable" extension (and "allowances") + /// Returns all allowances this owner has approved. Supports pagination. + /// Return type: AllAllowancesResponse. + AllAllowances { + owner: String, + start_after: Option, + limit: Option, + }, + /// Only with "enumerable" extension + /// Returns all accounts that have balances. Supports pagination. + /// Return type: AllAccountsResponse. + AllAccounts { + start_after: Option, + limit: Option, + }, +} diff --git a/wormhole/cw20-legacy/src/state.rs b/wormhole/cw20-legacy/src/state.rs new file mode 100644 index 00000000..db9543de --- /dev/null +++ b/wormhole/cw20-legacy/src/state.rs @@ -0,0 +1,73 @@ +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use cosmwasm_std::{CanonicalAddr, Uint128}; +use cw_storage_plus::{Item, Map}; + +use cw20::AllowanceResponse; + +#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] +#[serde(rename_all = "snake_case")] +pub struct TokenInfo { + pub name: String, + pub symbol: String, + pub decimals: u8, + pub total_supply: Uint128, + pub mint: Option, +} + +#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] +pub struct MinterData { + pub minter: CanonicalAddr, + /// cap is how many more tokens can be issued by the minter + pub cap: Option, +} + +impl TokenInfo { + pub fn get_cap(&self) -> Option { + self.mint.as_ref().and_then(|v| v.cap) + } +} + +pub const TOKEN_INFO: Item = Item::new("\u{0}\ntoken_info"); +pub const BALANCES: Map<&[u8], Uint128> = Map::new("balance"); +pub const ALLOWANCES: Map<(&[u8], &[u8]), AllowanceResponse> = Map::new("allowance"); + +#[cfg(test)] +mod test { + use super::*; + + use cosmwasm_std::testing::mock_dependencies; + use cosmwasm_std::{StdResult, Storage}; + use cosmwasm_storage::{singleton, singleton_read}; + + pub static TOKEN_INFO_KEY: &[u8] = b"token_info"; + + pub fn store_token_info(storage: &mut dyn Storage, token_info: &TokenInfo) -> StdResult<()> { + singleton(storage, TOKEN_INFO_KEY).save(token_info) + } + pub fn read_token_info(storage: &dyn Storage) -> StdResult { + singleton_read(storage, TOKEN_INFO_KEY).load() + } + + #[test] + fn token_info_legacy_compatibility() { + let mut deps = mock_dependencies(); + store_token_info( + &mut deps.storage, + &TokenInfo { + name: "test".to_string(), + symbol: "TEST".to_string(), + decimals: 6, + total_supply: Default::default(), + mint: None, + }, + ) + .unwrap(); + + assert_eq!( + TOKEN_INFO.load(&deps.storage).unwrap(), + read_token_info(&deps.storage).unwrap() + ); + } +} diff --git a/wormhole/cw20-wrapped/.cargo/config b/wormhole/cw20-wrapped/.cargo/config new file mode 100644 index 00000000..2d5cce4e --- /dev/null +++ b/wormhole/cw20-wrapped/.cargo/config @@ -0,0 +1,5 @@ +[alias] +wasm = "build --release --target wasm32-unknown-unknown" +wasm-debug = "build --target wasm32-unknown-unknown" +unit-test = "test --lib --features backtraces" +integration-test = "test --test integration" \ No newline at end of file diff --git a/wormhole/cw20-wrapped/Cargo.toml b/wormhole/cw20-wrapped/Cargo.toml new file mode 100644 index 00000000..cc537c86 --- /dev/null +++ b/wormhole/cw20-wrapped/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "cw20-wrapped" +version = "0.1.0" +authors = ["Yuriy Savchenko "] +edition = "2018" +description = "Wrapped CW20 token contract" + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +backtraces = ["cosmwasm-std/backtraces"] +# use library feature to disable all init/handle/query exports +library = [] + +[dependencies] +cosmwasm-std = "1.0.0" +cosmwasm-storage = "1.0.0" +cw-storage-plus = "0.13.4" +cw2 = "0.13.4" +cw20 = "0.13.4" +cw20-legacy = { path = "../cw20-legacy" } +schemars = "0.8.10" +serde = { version = "1.0.137", default-features = false, features = ["derive"] } +thiserror = "1.0.31" diff --git a/wormhole/cw20-wrapped/src/contract.rs b/wormhole/cw20-wrapped/src/contract.rs new file mode 100644 index 00000000..47e34a26 --- /dev/null +++ b/wormhole/cw20-wrapped/src/contract.rs @@ -0,0 +1,348 @@ +use cosmwasm_std::{ + to_binary, Binary, CosmosMsg, Deps, DepsMut, Env, MessageInfo, Response, StdError, StdResult, + Uint128, WasmMsg, +}; + +#[cfg(not(feature = "library"))] +use cosmwasm_std::entry_point; + +use cw2::set_contract_version; +use cw20_legacy::allowances::{ + execute_burn_from, execute_decrease_allowance, execute_increase_allowance, execute_send_from, + execute_transfer_from, query_allowance, +}; +use cw20_legacy::contract::{execute_mint, execute_send, execute_transfer, query_balance}; +use cw20_legacy::state::{MinterData, TokenInfo, TOKEN_INFO}; +use cw20_legacy::ContractError; + +use crate::msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg, WrappedAssetInfoResponse}; +use crate::state::{wrapped_asset_info, wrapped_asset_info_read, WrappedAssetInfo}; +use cw20::TokenInfoResponse; +use std::string::String; + +type HumanAddr = String; + +// version info for migration info +const CONTRACT_NAME: &str = "crates.io:cw20-base"; +const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn instantiate( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: InstantiateMsg, +) -> StdResult { + set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + + // store token info using cw20-base format + let data = TokenInfo { + name: msg.name, + symbol: msg.symbol, + decimals: msg.decimals, + total_supply: Uint128::new(0), + // set creator as minter + mint: Some(MinterData { + minter: deps.api.addr_canonicalize(info.sender.as_str())?, + cap: None, + }), + }; + TOKEN_INFO.save(deps.storage, &data)?; + + // save wrapped asset info + let data = WrappedAssetInfo { + asset_chain: msg.asset_chain, + asset_address: msg.asset_address, + bridge: deps.api.addr_canonicalize(info.sender.as_str())?, + }; + wrapped_asset_info(deps.storage).save(&data)?; + + if let Some(mint_info) = msg.mint { + execute_mint(deps, env, info, mint_info.recipient, mint_info.amount) + .map_err(|e| StdError::generic_err(format!("{}", e)))?; + } + + if let Some(hook) = msg.init_hook { + Ok( + Response::new().add_message(CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: hook.contract_addr, + msg: hook.msg, + funds: vec![], + })), + ) + } else { + Ok(Response::default()) + } +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn execute( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: ExecuteMsg, +) -> Result { + match msg { + // these all come from cw20-base to implement the cw20 standard + ExecuteMsg::Transfer { recipient, amount } => { + execute_transfer(deps, env, info, recipient, amount) + } + ExecuteMsg::Burn { account, amount } => execute_burn_from(deps, env, info, account, amount), + ExecuteMsg::Send { + contract, + amount, + msg, + } => execute_send(deps, env, info, contract, amount, msg), + ExecuteMsg::Mint { recipient, amount } => { + execute_mint_wrapped(deps, env, info, recipient, amount) + } + ExecuteMsg::IncreaseAllowance { + spender, + amount, + expires, + } => execute_increase_allowance(deps, env, info, spender, amount, expires), + ExecuteMsg::DecreaseAllowance { + spender, + amount, + expires, + } => execute_decrease_allowance(deps, env, info, spender, amount, expires), + ExecuteMsg::TransferFrom { + owner, + recipient, + amount, + } => execute_transfer_from(deps, env, info, owner, recipient, amount), + ExecuteMsg::BurnFrom { owner, amount } => execute_burn_from(deps, env, info, owner, amount), + ExecuteMsg::SendFrom { + owner, + contract, + amount, + msg, + } => execute_send_from(deps, env, info, owner, contract, amount, msg), + ExecuteMsg::UpdateMetadata { name, symbol } => { + execute_update_metadata(deps, env, info, name, symbol) + } + } +} + +fn execute_mint_wrapped( + deps: DepsMut, + env: Env, + info: MessageInfo, + recipient: HumanAddr, + amount: Uint128, +) -> Result { + // Only bridge can mint + let wrapped_info = wrapped_asset_info_read(deps.storage).load()?; + if wrapped_info.bridge != deps.api.addr_canonicalize(info.sender.as_str())? { + return Err(ContractError::Unauthorized {}); + } + + execute_mint(deps, env, info, recipient, amount) +} + +fn execute_update_metadata( + deps: DepsMut, + _env: Env, + info: MessageInfo, + name: String, + symbol: String, +) -> Result { + // Only bridge can update. + let wrapped_info = wrapped_asset_info_read(deps.storage).load()?; + if wrapped_info.bridge != deps.api.addr_canonicalize(info.sender.as_str())? { + return Err(ContractError::Unauthorized {}); + } + + let mut state = TOKEN_INFO.load(deps.storage)?; + state.name = name; + state.symbol = symbol; + TOKEN_INFO.save(deps.storage, &state)?; + Ok(Response::default()) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { + match msg { + QueryMsg::WrappedAssetInfo {} => to_binary(&query_wrapped_asset_info(deps)?), + // inherited from cw20-base + QueryMsg::TokenInfo {} => to_binary(&query_token_info(deps)?), + QueryMsg::Balance { address } => to_binary(&query_balance(deps, address)?), + QueryMsg::Allowance { owner, spender } => { + to_binary(&query_allowance(deps, owner, spender)?) + } + } +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn migrate(_deps: DepsMut, _env: Env, _msg: MigrateMsg) -> StdResult { + Ok(Response::new()) +} + +pub fn query_token_info(deps: Deps) -> StdResult { + let info = TOKEN_INFO.load(deps.storage)?; + Ok(TokenInfoResponse { + name: info.name + " (Wormhole)", + symbol: info.symbol, + decimals: info.decimals, + total_supply: info.total_supply, + }) +} + +pub fn query_wrapped_asset_info(deps: Deps) -> StdResult { + let info = wrapped_asset_info_read(deps.storage).load()?; + Ok(WrappedAssetInfoResponse { + asset_chain: info.asset_chain, + asset_address: info.asset_address, + bridge: deps.api.addr_humanize(&info.bridge)?, + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info}; + use cw20::TokenInfoResponse; + + fn get_balance(deps: Deps, address: HumanAddr) -> Uint128 { + query_balance(deps, address).unwrap().balance + } + + fn do_init(mut deps: DepsMut, creator: &HumanAddr) { + let init_msg = InstantiateMsg { + name: "Integers".to_string(), + symbol: "INT".to_string(), + asset_chain: 1, + asset_address: vec![1; 32].into(), + decimals: 10, + mint: None, + init_hook: None, + }; + let env = mock_env(); + let info = mock_info(creator, &[]); + let res = instantiate(deps.branch(), env, info, init_msg).unwrap(); + assert_eq!(0, res.messages.len()); + + assert_eq!( + query_token_info(deps.as_ref()).unwrap(), + TokenInfoResponse { + name: "Integers (Wormhole)".to_string(), + symbol: "INT".to_string(), + decimals: 10, + total_supply: Uint128::from(0u128), + } + ); + + assert_eq!( + query_wrapped_asset_info(deps.as_ref()).unwrap(), + WrappedAssetInfoResponse { + asset_chain: 1, + asset_address: vec![1; 32].into(), + bridge: deps.api.addr_validate(creator).unwrap(), + } + ); + } + + fn do_init_and_mint( + mut deps: DepsMut, + creator: &HumanAddr, + mint_to: &HumanAddr, + amount: Uint128, + ) { + do_init(deps.branch(), creator); + + let msg = ExecuteMsg::Mint { + recipient: mint_to.clone(), + amount, + }; + + let env = mock_env(); + let info = mock_info(creator, &[]); + let res = execute(deps.branch(), env, info, msg).unwrap(); + assert_eq!(0, res.messages.len()); + assert_eq!(get_balance(deps.as_ref(), mint_to.clone(),), amount); + + assert_eq!( + query_token_info(deps.as_ref()).unwrap(), + TokenInfoResponse { + name: "Integers (Wormhole)".to_string(), + symbol: "INT".to_string(), + decimals: 10, + total_supply: amount, + } + ); + } + + #[test] + fn can_mint_by_minter() { + let mut deps = mock_dependencies(); + let minter = HumanAddr::from("minter"); + let recipient = HumanAddr::from("recipient"); + let amount = Uint128::new(222_222_222); + do_init_and_mint(deps.as_mut(), &minter, &recipient, amount); + } + + #[test] + fn others_cannot_mint() { + let mut deps = mock_dependencies(); + let minter = HumanAddr::from("minter"); + let recipient = HumanAddr::from("recipient"); + do_init(deps.as_mut(), &minter); + + let amount = Uint128::new(222_222_222); + let msg = ExecuteMsg::Mint { recipient, amount }; + + let other_address = HumanAddr::from("other"); + let env = mock_env(); + let info = mock_info(&other_address, &[]); + let res = execute(deps.as_mut(), env, info, msg); + assert_eq!( + format!("{}", res.unwrap_err()), + format!("{}", crate::error::ContractError::Unauthorized {}) + ); + } + + #[test] + fn transfer_balance_success() { + let mut deps = mock_dependencies(); + let minter = HumanAddr::from("minter"); + let owner = HumanAddr::from("owner"); + let amount_initial = Uint128::new(222_222_222); + do_init_and_mint(deps.as_mut(), &minter, &owner, amount_initial); + + // Transfer + let recipient = HumanAddr::from("recipient"); + let amount_transfer = Uint128::new(222_222); + let msg = ExecuteMsg::Transfer { + recipient: recipient.clone(), + amount: amount_transfer, + }; + + let env = mock_env(); + let info = mock_info(&owner, &[]); + let res = execute(deps.as_mut(), env, info, msg).unwrap(); + assert_eq!(0, res.messages.len()); + assert_eq!(get_balance(deps.as_ref(), owner), Uint128::new(222_000_000)); + assert_eq!(get_balance(deps.as_ref(), recipient), amount_transfer); + } + + #[test] + fn transfer_balance_not_enough() { + let mut deps = mock_dependencies(); + let minter = HumanAddr::from("minter"); + let owner = HumanAddr::from("owner"); + let amount_initial = Uint128::new(222_221); + do_init_and_mint(deps.as_mut(), &minter, &owner, amount_initial); + + // Transfer + let recipient = HumanAddr::from("recipient"); + let amount_transfer = Uint128::new(222_222); + let msg = ExecuteMsg::Transfer { + recipient, + amount: amount_transfer, + }; + + let env = mock_env(); + let info = mock_info(&owner, &[]); + let _ = execute(deps.as_mut(), env, info, msg).unwrap_err(); // Will panic if no error + } +} diff --git a/wormhole/cw20-wrapped/src/error.rs b/wormhole/cw20-wrapped/src/error.rs new file mode 100644 index 00000000..95eba93c --- /dev/null +++ b/wormhole/cw20-wrapped/src/error.rs @@ -0,0 +1,27 @@ +use cosmwasm_std::StdError; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum ContractError { + // CW20 errors + #[error("{0}")] + Std(#[from] StdError), + + #[error("Unauthorized")] + Unauthorized {}, + + #[error("Cannot set to own account")] + CannotSetOwnAccount {}, + + #[error("Invalid zero amount")] + InvalidZeroAmount {}, + + #[error("Allowance is expired")] + Expired {}, + + #[error("No allowance for this account")] + NoAllowance {}, + + #[error("Minting cannot exceed the cap")] + CannotExceedCap {}, +} diff --git a/wormhole/cw20-wrapped/src/lib.rs b/wormhole/cw20-wrapped/src/lib.rs new file mode 100644 index 00000000..6e2ebd88 --- /dev/null +++ b/wormhole/cw20-wrapped/src/lib.rs @@ -0,0 +1,7 @@ +mod error; + +pub mod contract; +pub mod msg; +pub mod state; + +pub use crate::error::ContractError; diff --git a/wormhole/cw20-wrapped/src/msg.rs b/wormhole/cw20-wrapped/src/msg.rs new file mode 100644 index 00000000..bdab66cb --- /dev/null +++ b/wormhole/cw20-wrapped/src/msg.rs @@ -0,0 +1,121 @@ +#![allow(clippy::field_reassign_with_default)] +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use cosmwasm_std::{Addr, Binary, Uint128}; +use cw20::Expiration; + +type HumanAddr = String; + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +pub struct InstantiateMsg { + pub name: String, + pub symbol: String, + pub asset_chain: u16, + pub asset_address: Binary, + pub decimals: u8, + pub mint: Option, + pub init_hook: Option, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +pub struct InitHook { + pub msg: Binary, + pub contract_addr: HumanAddr, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +pub struct InitMint { + pub recipient: HumanAddr, + pub amount: Uint128, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub struct MigrateMsg {} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum ExecuteMsg { + /// Implements CW20. Transfer is a base message to move tokens to another account without triggering actions + Transfer { + recipient: HumanAddr, + amount: Uint128, + }, + /// Slightly different than CW20. Burn is a base message to destroy tokens forever + Burn { account: HumanAddr, amount: Uint128 }, + /// Implements CW20. Send is a base message to transfer tokens to a contract and trigger an action + /// on the receiving contract. + Send { + contract: HumanAddr, + amount: Uint128, + msg: Binary, + }, + /// Implements CW20 "mintable" extension. If authorized, creates amount new tokens + /// and adds to the recipient balance. + Mint { + recipient: HumanAddr, + amount: Uint128, + }, + /// Implements CW20 "approval" extension. Allows spender to access an additional amount tokens + /// from the owner's (env.sender) account. If expires is Some(), overwrites current allowance + /// expiration with this one. + IncreaseAllowance { + spender: HumanAddr, + amount: Uint128, + expires: Option, + }, + /// Implements CW20 "approval" extension. Lowers the spender's access of tokens + /// from the owner's (env.sender) account by amount. If expires is Some(), overwrites current + /// allowance expiration with this one. + DecreaseAllowance { + spender: HumanAddr, + amount: Uint128, + expires: Option, + }, + /// Implements CW20 "approval" extension. Transfers amount tokens from owner -> recipient + /// if `env.sender` has sufficient pre-approval. + TransferFrom { + owner: HumanAddr, + recipient: HumanAddr, + amount: Uint128, + }, + /// Implements CW20 "approval" extension. Sends amount tokens from owner -> contract + /// if `env.sender` has sufficient pre-approval. + SendFrom { + owner: HumanAddr, + contract: HumanAddr, + amount: Uint128, + msg: Binary, + }, + /// Implements CW20 "approval" extension. Destroys tokens forever + BurnFrom { owner: HumanAddr, amount: Uint128 }, + /// Extend Interface with the ability to update token metadata. + UpdateMetadata { name: String, symbol: String }, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum QueryMsg { + // Generic information about the wrapped asset + WrappedAssetInfo {}, + /// Implements CW20. Returns the current balance of the given address, 0 if unset. + Balance { + address: HumanAddr, + }, + /// Implements CW20. Returns metadata on the contract - name, decimals, supply, etc. + TokenInfo {}, + /// Implements CW20 "allowance" extension. + /// Returns how much spender can use from owner account, 0 if unset. + Allowance { + owner: HumanAddr, + spender: HumanAddr, + }, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +pub struct WrappedAssetInfoResponse { + pub asset_chain: u16, // Asset chain id + pub asset_address: Binary, // Asset smart contract address in the original chain + pub bridge: Addr, // Bridge address, authorized to mint and burn wrapped tokens +} diff --git a/wormhole/cw20-wrapped/src/state.rs b/wormhole/cw20-wrapped/src/state.rs new file mode 100644 index 00000000..8e8f4854 --- /dev/null +++ b/wormhole/cw20-wrapped/src/state.rs @@ -0,0 +1,23 @@ +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use cosmwasm_std::{Binary, CanonicalAddr, Storage}; +use cosmwasm_storage::{singleton, singleton_read, ReadonlySingleton, Singleton}; + +pub const KEY_WRAPPED_ASSET: &[u8] = b"wrappedAsset"; + +// Created at initialization and reference original asset and bridge address +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +pub struct WrappedAssetInfo { + pub asset_chain: u16, // Asset chain id + pub asset_address: Binary, // Asset smart contract address on the original chain + pub bridge: CanonicalAddr, // Bridge address, authorized to mint and burn wrapped tokens +} + +pub fn wrapped_asset_info(storage: &mut dyn Storage) -> Singleton { + singleton(storage, KEY_WRAPPED_ASSET) +} + +pub fn wrapped_asset_info_read(storage: &dyn Storage) -> ReadonlySingleton { + singleton_read(storage, KEY_WRAPPED_ASSET) +} diff --git a/wormhole/cw20-wrapped/tests/integration.rs b/wormhole/cw20-wrapped/tests/integration.rs new file mode 100644 index 00000000..5f22318d --- /dev/null +++ b/wormhole/cw20-wrapped/tests/integration.rs @@ -0,0 +1,174 @@ +use cosmwasm_std::testing::{ + mock_dependencies, mock_env, mock_info, MockApi, MockQuerier, MockStorage, +}; +use cosmwasm_std::{from_slice, Addr, Api, OwnedDeps, Response, Storage, Uint128}; +use cosmwasm_storage::to_length_prefixed; +use cw20::TokenInfoResponse; +use cw20_wrapped::contract::{execute, instantiate, query}; +use cw20_wrapped::msg::{ExecuteMsg, InstantiateMsg, QueryMsg, WrappedAssetInfoResponse}; +use cw20_wrapped::state::{WrappedAssetInfo, KEY_WRAPPED_ASSET}; +use cw20_wrapped::ContractError; + +static INITIALIZER: &str = "addr0000"; +static RECIPIENT: &str = "addr2222"; +static SENDER: &str = "addr3333"; + +fn get_wrapped_asset_info(storage: &S) -> WrappedAssetInfo { + let key = to_length_prefixed(KEY_WRAPPED_ASSET); + let data = storage.get(&key).expect("data should exist"); + from_slice(&data).expect("invalid data") +} + +fn do_init() -> OwnedDeps { + let mut deps = mock_dependencies(); + let init_msg = InstantiateMsg { + name: "Integers".into(), + symbol: "INT".into(), + asset_chain: 1, + asset_address: vec![1; 32].into(), + decimals: 10, + mint: None, + init_hook: None, + }; + let env = mock_env(); + let info = mock_info(INITIALIZER, &[]); + let res: Response = instantiate(deps.as_mut(), env, info, init_msg).unwrap(); + assert_eq!(0, res.messages.len()); + + // query the store directly + let bridge = deps.api.addr_canonicalize(INITIALIZER).unwrap(); + assert_eq!( + get_wrapped_asset_info(&deps.storage), + WrappedAssetInfo { + asset_chain: 1, + asset_address: vec![1; 32].into(), + bridge, + } + ); + + deps +} + +fn do_mint( + deps: &mut OwnedDeps, + recipient: &Addr, + amount: &Uint128, +) { + let mint_msg = ExecuteMsg::Mint { + recipient: recipient.to_string(), + amount: *amount, + }; + let info = mock_info(INITIALIZER, &[]); + let handle_response: Response = execute(deps.as_mut(), mock_env(), info, mint_msg).unwrap(); + assert_eq!(0, handle_response.messages.len()); +} + +fn do_transfer( + deps: &mut OwnedDeps, + sender: &Addr, + recipient: &Addr, + amount: &Uint128, +) { + let transfer_msg = ExecuteMsg::Transfer { + recipient: recipient.to_string(), + amount: *amount, + }; + let env = mock_env(); + let info = mock_info(sender.as_str(), &[]); + let handle_response: Response = execute(deps.as_mut(), env, info, transfer_msg).unwrap(); + assert_eq!(0, handle_response.messages.len()); +} + +fn check_balance( + deps: &OwnedDeps, + address: &Addr, + amount: &Uint128, +) { + let query_response = query( + deps.as_ref(), + mock_env(), + QueryMsg::Balance { + address: address.to_string(), + }, + ) + .unwrap(); + assert_eq!( + query_response.as_slice(), + format!("{{\"balance\":\"{}\"}}", amount.u128()).as_bytes() + ); +} + +fn check_token_details(deps: &OwnedDeps, supply: Uint128) { + let query_response = query(deps.as_ref(), mock_env(), QueryMsg::TokenInfo {}).unwrap(); + assert_eq!( + from_slice::(query_response.as_slice()).unwrap(), + TokenInfoResponse { + name: "Integers (Wormhole)".into(), + symbol: "INT".into(), + decimals: 10, + total_supply: supply, + } + ); +} + +#[test] +fn init_works() { + let deps = do_init(); + check_token_details(&deps, Uint128::new(0)); +} + +#[test] +fn query_works() { + let deps = do_init(); + + let query_response = query(deps.as_ref(), mock_env(), QueryMsg::WrappedAssetInfo {}).unwrap(); + assert_eq!( + from_slice::(&query_response).unwrap(), + WrappedAssetInfoResponse { + asset_chain: 1, + asset_address: vec![1; 32].into(), + bridge: Addr::unchecked(INITIALIZER), + } + ); +} + +#[test] +fn mint_works() { + let mut deps = do_init(); + + let recipient = Addr::unchecked(RECIPIENT); + do_mint(&mut deps, &recipient, &Uint128::new(123_123_123)); + + check_balance(&deps, &recipient, &Uint128::new(123_123_123)); + check_token_details(&deps, Uint128::new(123_123_123)); +} + +#[test] +fn others_cannot_mint() { + let mut deps = do_init(); + + let mint_msg = ExecuteMsg::Mint { + recipient: RECIPIENT.into(), + amount: Uint128::new(123_123_123), + }; + let env = mock_env(); + let info = mock_info(RECIPIENT, &[]); + let handle_result = execute(deps.as_mut(), env, info, mint_msg); + assert_eq!( + format!("{}", handle_result.unwrap_err()), + format!("{}", ContractError::Unauthorized {}) + ); +} + +#[test] +fn transfer_works() { + let mut deps = do_init(); + + let sender = Addr::unchecked(SENDER); + let recipient = Addr::unchecked(RECIPIENT); + do_mint(&mut deps, &sender, &Uint128::new(123_123_123)); + do_transfer(&mut deps, &sender, &recipient, &Uint128::new(123_123_000)); + + check_balance(&deps, &sender, &Uint128::new(123)); + check_balance(&deps, &recipient, &Uint128::new(123_123_000)); +} diff --git a/wormhole/cw721-base/.cargo/config b/wormhole/cw721-base/.cargo/config new file mode 100644 index 00000000..7d1a066c --- /dev/null +++ b/wormhole/cw721-base/.cargo/config @@ -0,0 +1,5 @@ +[alias] +wasm = "build --release --target wasm32-unknown-unknown" +wasm-debug = "build --target wasm32-unknown-unknown" +unit-test = "test --lib" +schema = "run --example schema" diff --git a/wormhole/cw721-base/Cargo.toml b/wormhole/cw721-base/Cargo.toml new file mode 100644 index 00000000..f591fc36 --- /dev/null +++ b/wormhole/cw721-base/Cargo.toml @@ -0,0 +1,38 @@ +[package] +name = "cw721-base" +version = "0.10.1" +authors = ["Ethan Frey "] +edition = "2018" +description = "Basic implementation cw721 NFTs" +license = "Apache-2.0" +repository = "https://github.com/CosmWasm/cw-nfts" +homepage = "https://cosmwasm.com" +documentation = "https://docs.cosmwasm.com" + +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. + "artifacts/*", +] + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +# for more explicit tests, cargo test --features=backtraces +backtraces = ["cosmwasm-std/backtraces"] +# use library feature to disable all instantiate/execute/query exports +library = [] + +[dependencies] +cw0 = "0.10.3" +cw2 = "0.13.4" +cw721 = { path = "../../packages/cw721", version = "0.10.1" } +cw-storage-plus = "0.13.4" +cosmwasm-std = "1.0.0" +schemars = "0.8.10" +serde = { version = "1.0.137", default-features = false, features = ["derive"] } +thiserror = "1.0.31" + +[dev-dependencies] +cosmwasm-schema = "1.0.0" diff --git a/wormhole/cw721-base/NOTICE b/wormhole/cw721-base/NOTICE new file mode 100644 index 00000000..7c303abc --- /dev/null +++ b/wormhole/cw721-base/NOTICE @@ -0,0 +1,14 @@ +Cw721_base +Copyright (C) 2020-2021 Confio OÜ + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/wormhole/cw721-base/README.md b/wormhole/cw721-base/README.md new file mode 100644 index 00000000..b245c922 --- /dev/null +++ b/wormhole/cw721-base/README.md @@ -0,0 +1,71 @@ +# Cw721 Basic + +This is a basic implementation of a cw721 NFT contract. It implements +the [CW721 spec](../../packages/cw721/README.md) and is designed to +be deployed as is, or imported into other contracts to easily build +cw721-compatible NFTs with custom logic. + +Implements: + +- [x] CW721 Base +- [x] Metadata extension +- [ ] Enumerable extension (AllTokens done, but not Tokens - requires [#81](https://github.com/CosmWasm/cw-plus/issues/81)) + +## Implementation + +The `ExecuteMsg` and `QueryMsg` implementations follow the [CW721 spec](../../packages/cw721/README.md) and are described there. +Beyond that, we make a few additions: + +* `InstantiateMsg` takes name and symbol (for metadata), as well as a **Minter** address. This is a special address that has full +power to mint new NFTs (but not modify existing ones) +* `ExecuteMsg::Mint{token_id, owner, token_uri}` - creates a new token with given owner and (optional) metadata. It can only be called by +the Minter set in `instantiate`. +* `QueryMsg::Minter{}` - returns the minter address for this contract. + +It requires all tokens to have defined metadata in the standard format (with no extensions). For generic NFTs this may +often be enough. + +The *Minter* can either be an external actor (eg. web server, using PubKey) or another contract. If you just want to customize +the minting behavior but not other functionality, you could extend this contract (importing code and wiring it together) +or just create a custom contract as the owner and use that contract to Mint. + +If provided, it is expected that the _token_uri_ points to a JSON file following the [ERC721 Metadata JSON Schema](https://eips.ethereum.org/EIPS/eip-721). + +## Running this contract + +You will need Rust 1.44.1+ with `wasm32-unknown-unknown` target installed. + +You can run unit tests on this via: + +`cargo test` + +Once you are happy with the content, you can compile it to wasm via: + +``` +RUSTFLAGS='-C link-arg=-s' cargo wasm +cp ../../target/wasm32-unknown-unknown/release/cw721_base.wasm . +ls -l cw721_base.wasm +sha256sum cw721_base.wasm +``` + +Or for a production-ready (optimized) build, run a build command in the +the repository root: https://github.com/CosmWasm/cw-plus#compiling. + +## Importing this contract + +You can also import much of the logic of this contract to build another +CW721-compliant contract, such as tradable names, crypto kitties, +or tokenized real estate. + +Basically, you just need to write your handle function and import +`cw721_base::contract::handle_transfer`, etc and dispatch to them. +This allows you to use custom `ExecuteMsg` and `QueryMsg` with your additional +calls, but then use the underlying implementation for the standard cw721 +messages you want to support. The same with `QueryMsg`. You will most +likely want to write a custom, domain-specific `instantiate`. + +**TODO: add example when written** + +For now, you can look at [`cw721-staking`](../cw721-staking/README.md) +for an example of how to "inherit" cw721 functionality and combine it with custom logic. +The process is similar for cw721. diff --git a/wormhole/cw721-base/examples/cw721_base_schema.rs b/wormhole/cw721-base/examples/cw721_base_schema.rs new file mode 100644 index 00000000..c315be66 --- /dev/null +++ b/wormhole/cw721-base/examples/cw721_base_schema.rs @@ -0,0 +1,39 @@ +use std::env::current_dir; +use std::fs::create_dir_all; + +use cosmwasm_schema::{export_schema, export_schema_with_title, remove_schemas, schema_for}; + +use cw721::{ + AllNftInfoResponse, ApprovalResponse, ApprovalsResponse, ContractInfoResponse, NftInfoResponse, + NumTokensResponse, OperatorsResponse, OwnerOfResponse, TokensResponse, +}; +use cw721_base::{ExecuteMsg, Extension, InstantiateMsg, MinterResponse, QueryMsg}; + +fn main() { + let mut out_dir = current_dir().unwrap(); + out_dir.push("schema"); + create_dir_all(&out_dir).unwrap(); + remove_schemas(&out_dir).unwrap(); + + export_schema(&schema_for!(InstantiateMsg), &out_dir); + export_schema_with_title(&schema_for!(ExecuteMsg), &out_dir, "ExecuteMsg"); + export_schema(&schema_for!(QueryMsg), &out_dir); + export_schema_with_title( + &schema_for!(AllNftInfoResponse), + &out_dir, + "AllNftInfoResponse", + ); + export_schema(&schema_for!(ApprovalResponse), &out_dir); + export_schema(&schema_for!(ApprovalsResponse), &out_dir); + export_schema(&schema_for!(OperatorsResponse), &out_dir); + export_schema(&schema_for!(ContractInfoResponse), &out_dir); + export_schema(&schema_for!(MinterResponse), &out_dir); + export_schema_with_title( + &schema_for!(NftInfoResponse), + &out_dir, + "NftInfoResponse", + ); + export_schema(&schema_for!(NumTokensResponse), &out_dir); + export_schema(&schema_for!(OwnerOfResponse), &out_dir); + export_schema(&schema_for!(TokensResponse), &out_dir); +} diff --git a/wormhole/cw721-base/helpers.ts b/wormhole/cw721-base/helpers.ts new file mode 100644 index 00000000..10bcaa71 --- /dev/null +++ b/wormhole/cw721-base/helpers.ts @@ -0,0 +1,393 @@ +import axios from "axios"; +import fs from "fs"; +import { SigningCosmWasmClient } from "@cosmjs/cosmwasm-stargate"; +import { GasPrice, calculateFee, StdFee } from "@cosmjs/stargate"; +import { DirectSecp256k1HdWallet, makeCosmoshubPath } from "@cosmjs/proto-signing"; +import { Slip10RawIndex } from "@cosmjs/crypto"; +import path from "path"; +/* + * This is a set of helpers meant for use with @cosmjs/cli + * With these you can easily use the cw721 contract without worrying about forming messages and parsing queries. + * + * Usage: npx @cosmjs/cli@^0.26 --init https://raw.githubusercontent.com/CosmWasm/cosmwasm-plus/master/contracts/cw721-base/helpers.ts + * + * Create a client: + * const [addr, client] = await useOptions(pebblenetOptions).setup('password'); + * + * Get the mnemonic: + * await useOptions(pebblenetOptions).recoverMnemonic(password); + * + * Create contract: + * const contract = CW721(client, pebblenetOptions.fees); + * + * Upload contract: + * const codeId = await contract.upload(addr); + * + * Instantiate contract example: + * const initMsg = { + * name: "Potato Coin", + * symbol: "TATER", + * minter: addr + * }; + * const instance = await contract.instantiate(addr, codeId, initMsg, 'Potato Coin!'); + * If you want to use this code inside an app, you will need several imports from https://github.com/CosmWasm/cosmjs + */ + +interface Options { + readonly httpUrl: string + readonly networkId: string + readonly feeToken: string + readonly bech32prefix: string + readonly hdPath: readonly Slip10RawIndex[] + readonly faucetUrl?: string + readonly defaultKeyFile: string, + readonly fees: { + upload: StdFee, + init: StdFee, + exec: StdFee + } +} + +const pebblenetGasPrice = GasPrice.fromString("0.01upebble"); +const pebblenetOptions: Options = { + httpUrl: 'https://rpc.pebblenet.cosmwasm.com', + networkId: 'pebblenet-1', + bech32prefix: 'wasm', + feeToken: 'upebble', + faucetUrl: 'https://faucet.pebblenet.cosmwasm.com/credit', + hdPath: makeCosmoshubPath(0), + defaultKeyFile: path.join(process.env.HOME, ".pebblenet.key"), + fees: { + upload: calculateFee(1500000, pebblenetGasPrice), + init: calculateFee(500000, pebblenetGasPrice), + exec: calculateFee(200000, pebblenetGasPrice), + }, +} + +interface Network { + setup: (password: string, filename?: string) => Promise<[string, SigningCosmWasmClient]> + recoverMnemonic: (password: string, filename?: string) => Promise +} + +const useOptions = (options: Options): Network => { + + const loadOrCreateWallet = async (options: Options, filename: string, password: string): Promise => { + let encrypted: string; + try { + encrypted = fs.readFileSync(filename, 'utf8'); + } catch (err) { + // generate if no file exists + const wallet = await DirectSecp256k1HdWallet.generate(12, {hdPaths: [options.hdPath], prefix: options.bech32prefix}); + const encrypted = await wallet.serialize(password); + fs.writeFileSync(filename, encrypted, 'utf8'); + return wallet; + } + // otherwise, decrypt the file (we cannot put deserialize inside try or it will over-write on a bad password) + const wallet = await DirectSecp256k1HdWallet.deserialize(encrypted, password); + return wallet; + }; + + const connect = async ( + wallet: DirectSecp256k1HdWallet, + options: Options + ): Promise => { + const clientOptions = { + prefix: options.bech32prefix + } + return await SigningCosmWasmClient.connectWithSigner(options.httpUrl, wallet, clientOptions) + }; + + const hitFaucet = async ( + faucetUrl: string, + address: string, + denom: string + ): Promise => { + await axios.post(faucetUrl, { denom, address }); + } + + const setup = async (password: string, filename?: string): Promise<[string, SigningCosmWasmClient]> => { + const keyfile = filename || options.defaultKeyFile; + const wallet = await loadOrCreateWallet(pebblenetOptions, keyfile, password); + const client = await connect(wallet, pebblenetOptions); + + const [account] = await wallet.getAccounts(); + // ensure we have some tokens + if (options.faucetUrl) { + const tokens = await client.getBalance(account.address, options.feeToken) + if (tokens.amount === '0') { + console.log(`Getting ${options.feeToken} from faucet`); + await hitFaucet(options.faucetUrl, account.address, options.feeToken); + } + } + + return [account.address, client]; + } + + const recoverMnemonic = async (password: string, filename?: string): Promise => { + const keyfile = filename || options.defaultKeyFile; + const wallet = await loadOrCreateWallet(pebblenetOptions, keyfile, password); + return wallet.mnemonic; + } + + return { setup, recoverMnemonic }; +} + +type TokenId = string + +interface Balances { + readonly address: string + readonly amount: string // decimal as string +} + +interface MintInfo { + readonly minter: string + readonly cap?: string // decimal as string +} + +interface ContractInfo { + readonly name: string + readonly symbol: string +} + +interface NftInfo { + readonly name: string, + readonly description: string, + readonly image: any +} + +interface Access { + readonly owner: string, + readonly approvals: [] +} +interface AllNftInfo { + readonly access: Access, + readonly info: NftInfo +} + +interface Operators { + readonly operators: [] +} + +interface Count { + readonly count: number +} + +interface InitMsg { + readonly name: string + readonly symbol: string + readonly minter: string +} +// Better to use this interface? +interface MintMsg { + readonly token_id: TokenId + readonly owner: string + readonly name: string + readonly description?: string + readonly image?: string +} + +type Expiration = { readonly at_height: number } | { readonly at_time: number } | { readonly never: {} }; + +interface AllowanceResponse { + readonly allowance: string; // integer as string + readonly expires: Expiration; +} + +interface AllowanceInfo { + readonly allowance: string; // integer as string + readonly spender: string; // bech32 address + readonly expires: Expiration; +} + +interface AllAllowancesResponse { + readonly allowances: readonly AllowanceInfo[]; +} + +interface AllAccountsResponse { + // list of bech32 address that have a balance + readonly accounts: readonly string[]; +} + +interface TokensResponse { + readonly tokens: readonly string[]; +} + +interface CW721Instance { + readonly contractAddress: string + + // queries + allowance: (owner: string, spender: string) => Promise + allAllowances: (owner: string, startAfter?: string, limit?: number) => Promise + allAccounts: (startAfter?: string, limit?: number) => Promise + minter: () => Promise + contractInfo: () => Promise + nftInfo: (tokenId: TokenId) => Promise + allNftInfo: (tokenId: TokenId) => Promise + ownerOf: (tokenId: TokenId) => Promise + approvedForAll: (owner: string, include_expired?: boolean, start_after?: string, limit?: number) => Promise + numTokens: () => Promise + tokens: (owner: string, startAfter?: string, limit?: number) => Promise + allTokens: (startAfter?: string, limit?: number) => Promise + + // actions + mint: (senderAddress: string, tokenId: TokenId, owner: string, name: string, level: number, description?: string, image?: string) => Promise + transferNft: (senderAddress: string, recipient: string, tokenId: TokenId) => Promise + sendNft: (senderAddress: string, contract: string, token_id: TokenId, msg?: BinaryType) => Promise + approve: (senderAddress: string, spender: string, tokenId: TokenId, expires?: Expiration) => Promise + approveAll: (senderAddress: string, operator: string, expires?: Expiration) => Promise + revoke: (senderAddress: string, spender: string, tokenId: TokenId) => Promise + revokeAll: (senderAddress: string, operator: string) => Promise +} + +interface CW721Contract { + // upload a code blob and returns a codeId + upload: (senderAddress: string) => Promise + + // instantiates a cw721 contract + // codeId must come from a previous deploy + // label is the public name of the contract in listing + // if you set admin, you can run migrations on this contract (likely client.senderAddress) + instantiate: (senderAddress: string, codeId: number, initMsg: Record, label: string, admin?: string) => Promise + + use: (contractAddress: string) => CW721Instance +} + + +export const CW721 = (client: SigningCosmWasmClient, fees: Options['fees']): CW721Contract => { + const use = (contractAddress: string): CW721Instance => { + + const allowance = async (owner: string, spender: string): Promise => { + return client.queryContractSmart(contractAddress, { allowance: { owner, spender } }); + }; + + const allAllowances = async (owner: string, startAfter?: string, limit?: number): Promise => { + return client.queryContractSmart(contractAddress, { all_allowances: { owner, start_after: startAfter, limit } }); + }; + + const allAccounts = async (startAfter?: string, limit?: number): Promise => { + const accounts: AllAccountsResponse = await client.queryContractSmart(contractAddress, { all_accounts: { start_after: startAfter, limit } }); + return accounts.accounts; + }; + + const minter = async (): Promise => { + return client.queryContractSmart(contractAddress, { minter: {} }); + }; + + const contractInfo = async (): Promise => { + return client.queryContractSmart(contractAddress, { contract_info: {} }); + }; + + const nftInfo = async (token_id: TokenId): Promise => { + return client.queryContractSmart(contractAddress, { nft_info: { token_id } }); + } + + const allNftInfo = async (token_id: TokenId): Promise => { + return client.queryContractSmart(contractAddress, { all_nft_info: { token_id } }); + } + + const ownerOf = async (token_id: TokenId): Promise => { + return await client.queryContractSmart(contractAddress, { owner_of: { token_id } }); + } + + const approvedForAll = async (owner: string, include_expired?: boolean, start_after?: string, limit?: number): Promise => { + return await client.queryContractSmart(contractAddress, { approved_for_all: { owner, include_expired, start_after, limit } }) + } + + // total number of tokens issued + const numTokens = async (): Promise => { + return client.queryContractSmart(contractAddress, { num_tokens: {} }); + } + + // list all token_ids that belong to a given owner + const tokens = async (owner: string, start_after?: string, limit?: number): Promise => { + return client.queryContractSmart(contractAddress, { tokens: { owner, start_after, limit } }); + } + + const allTokens = async (start_after?: string, limit?: number): Promise => { + return client.queryContractSmart(contractAddress, { all_tokens: { start_after, limit } }); + } + + // actions + const mint = async (senderAddress: string, token_id: TokenId, owner: string, name: string, level: number, description?: string, image?: string): Promise => { + const result = await client.execute(senderAddress, contractAddress, { mint: { token_id, owner, name, level, description, image } }, fees.exec); + return result.transactionHash; + } + + // transfers ownership, returns transactionHash + const transferNft = async (senderAddress: string, recipient: string, token_id: TokenId): Promise => { + const result = await client.execute(senderAddress, contractAddress, { transfer_nft: { recipient, token_id } }, fees.exec); + return result.transactionHash; + } + + // sends an nft token to another contract (TODO: msg type any needs to be revisited once receiveNft is implemented) + const sendNft = async (senderAddress: string, contract: string, token_id: TokenId, msg?: any): Promise => { + const result = await client.execute(senderAddress, contractAddress, { send_nft: { contract, token_id, msg } }, fees.exec) + return result.transactionHash; + } + + const approve = async (senderAddress: string, spender: string, token_id: TokenId, expires?: Expiration): Promise => { + const result = await client.execute(senderAddress, contractAddress, { approve: { spender, token_id, expires } }, fees.exec); + return result.transactionHash; + } + + const approveAll = async (senderAddress: string, operator: string, expires?: Expiration): Promise => { + const result = await client.execute(senderAddress, contractAddress, { approve_all: { operator, expires } }, fees.exec) + return result.transactionHash + } + + const revoke = async (senderAddress: string, spender: string, token_id: TokenId): Promise => { + const result = await client.execute(senderAddress, contractAddress, { revoke: { spender, token_id } }, fees.exec); + return result.transactionHash; + } + + const revokeAll = async (senderAddress: string, operator: string): Promise => { + const result = await client.execute(senderAddress, contractAddress, { revoke_all: { operator } }, fees.exec) + return result.transactionHash; + } + + return { + contractAddress, + allowance, + allAllowances, + allAccounts, + minter, + contractInfo, + nftInfo, + allNftInfo, + ownerOf, + approvedForAll, + numTokens, + tokens, + allTokens, + mint, + transferNft, + sendNft, + approve, + approveAll, + revoke, + revokeAll + }; + } + + const downloadWasm = async (url: string): Promise => { + const r = await axios.get(url, { responseType: 'arraybuffer' }) + if (r.status !== 200) { + throw new Error(`Download error: ${r.status}`) + } + return r.data + } + + const upload = async (senderAddress: string): Promise => { + const sourceUrl = "https://github.com/CosmWasm/cosmwasm-plus/releases/download/v0.9.0/cw721_base.wasm"; + const wasm = await downloadWasm(sourceUrl); + const result = await client.upload(senderAddress, wasm, fees.upload); + return result.codeId; + } + + const instantiate = async (senderAddress: string, codeId: number, initMsg: Record, label: string, admin?: string): Promise => { + const result = await client.instantiate(senderAddress, codeId, initMsg, label, fees.init, { memo: `Init ${label}`, admin }); + return use(result.contractAddress); + } + + return { upload, instantiate, use }; +} diff --git a/wormhole/cw721-base/schema/all_nft_info_response.json b/wormhole/cw721-base/schema/all_nft_info_response.json new file mode 100644 index 00000000..bfc334bf --- /dev/null +++ b/wormhole/cw721-base/schema/all_nft_info_response.json @@ -0,0 +1,155 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "AllNftInfoResponse", + "type": "object", + "required": [ + "access", + "info" + ], + "properties": { + "access": { + "description": "Who can transfer the token", + "allOf": [ + { + "$ref": "#/definitions/OwnerOfResponse" + } + ] + }, + "info": { + "description": "Data on the token itself,", + "allOf": [ + { + "$ref": "#/definitions/NftInfoResponse_for_Nullable_Empty" + } + ] + } + }, + "definitions": { + "Approval": { + "type": "object", + "required": [ + "expires", + "spender" + ], + "properties": { + "expires": { + "description": "When the Approval expires (maybe Expiration::never)", + "allOf": [ + { + "$ref": "#/definitions/Expiration" + } + ] + }, + "spender": { + "description": "Account that can transfer/send the token", + "type": "string" + } + } + }, + "Empty": { + "description": "An empty struct that serves as a placeholder in different places, such as contracts that don't set a custom message.\n\nIt is designed to be expressable in correct JSON and JSON Schema but contains no meaningful data. Previously we used enums without cases, but those cannot represented as valid JSON Schema (https://github.com/CosmWasm/cosmwasm/issues/451)", + "type": "object" + }, + "Expiration": { + "description": "Expiration represents a point in time when some event happens. It can compare with a BlockInfo and will return is_expired() == true once the condition is hit (and for every block in the future)", + "oneOf": [ + { + "description": "AtHeight will expire when `env.block.height` >= height", + "type": "object", + "required": [ + "at_height" + ], + "properties": { + "at_height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + { + "description": "AtTime will expire when `env.block.time` >= time", + "type": "object", + "required": [ + "at_time" + ], + "properties": { + "at_time": { + "$ref": "#/definitions/Timestamp" + } + }, + "additionalProperties": false + }, + { + "description": "Never will never expire. Used to express the empty variant", + "type": "object", + "required": [ + "never" + ], + "properties": { + "never": { + "type": "object" + } + }, + "additionalProperties": false + } + ] + }, + "NftInfoResponse_for_Nullable_Empty": { + "type": "object", + "properties": { + "extension": { + "description": "You can add any custom metadata here when you extend cw721-base", + "anyOf": [ + { + "$ref": "#/definitions/Empty" + }, + { + "type": "null" + } + ] + }, + "token_uri": { + "description": "Universal resource identifier for this NFT Should point to a JSON file that conforms to the ERC721 Metadata JSON Schema", + "type": [ + "string", + "null" + ] + } + } + }, + "OwnerOfResponse": { + "type": "object", + "required": [ + "approvals", + "owner" + ], + "properties": { + "approvals": { + "description": "If set this address is approved to transfer/send the token as well", + "type": "array", + "items": { + "$ref": "#/definitions/Approval" + } + }, + "owner": { + "description": "Owner of the token", + "type": "string" + } + } + }, + "Timestamp": { + "description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```", + "allOf": [ + { + "$ref": "#/definitions/Uint64" + } + ] + }, + "Uint64": { + "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```", + "type": "string" + } + } +} diff --git a/wormhole/cw721-base/schema/approval_response.json b/wormhole/cw721-base/schema/approval_response.json new file mode 100644 index 00000000..4f45b42e --- /dev/null +++ b/wormhole/cw721-base/schema/approval_response.json @@ -0,0 +1,94 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ApprovalResponse", + "type": "object", + "required": [ + "approval" + ], + "properties": { + "approval": { + "$ref": "#/definitions/Approval" + } + }, + "definitions": { + "Approval": { + "type": "object", + "required": [ + "expires", + "spender" + ], + "properties": { + "expires": { + "description": "When the Approval expires (maybe Expiration::never)", + "allOf": [ + { + "$ref": "#/definitions/Expiration" + } + ] + }, + "spender": { + "description": "Account that can transfer/send the token", + "type": "string" + } + } + }, + "Expiration": { + "description": "Expiration represents a point in time when some event happens. It can compare with a BlockInfo and will return is_expired() == true once the condition is hit (and for every block in the future)", + "oneOf": [ + { + "description": "AtHeight will expire when `env.block.height` >= height", + "type": "object", + "required": [ + "at_height" + ], + "properties": { + "at_height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + { + "description": "AtTime will expire when `env.block.time` >= time", + "type": "object", + "required": [ + "at_time" + ], + "properties": { + "at_time": { + "$ref": "#/definitions/Timestamp" + } + }, + "additionalProperties": false + }, + { + "description": "Never will never expire. Used to express the empty variant", + "type": "object", + "required": [ + "never" + ], + "properties": { + "never": { + "type": "object" + } + }, + "additionalProperties": false + } + ] + }, + "Timestamp": { + "description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```", + "allOf": [ + { + "$ref": "#/definitions/Uint64" + } + ] + }, + "Uint64": { + "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```", + "type": "string" + } + } +} diff --git a/wormhole/cw721-base/schema/approvals_response.json b/wormhole/cw721-base/schema/approvals_response.json new file mode 100644 index 00000000..8d8e39ea --- /dev/null +++ b/wormhole/cw721-base/schema/approvals_response.json @@ -0,0 +1,97 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ApprovalsResponse", + "type": "object", + "required": [ + "approvals" + ], + "properties": { + "approvals": { + "type": "array", + "items": { + "$ref": "#/definitions/Approval" + } + } + }, + "definitions": { + "Approval": { + "type": "object", + "required": [ + "expires", + "spender" + ], + "properties": { + "expires": { + "description": "When the Approval expires (maybe Expiration::never)", + "allOf": [ + { + "$ref": "#/definitions/Expiration" + } + ] + }, + "spender": { + "description": "Account that can transfer/send the token", + "type": "string" + } + } + }, + "Expiration": { + "description": "Expiration represents a point in time when some event happens. It can compare with a BlockInfo and will return is_expired() == true once the condition is hit (and for every block in the future)", + "oneOf": [ + { + "description": "AtHeight will expire when `env.block.height` >= height", + "type": "object", + "required": [ + "at_height" + ], + "properties": { + "at_height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + { + "description": "AtTime will expire when `env.block.time` >= time", + "type": "object", + "required": [ + "at_time" + ], + "properties": { + "at_time": { + "$ref": "#/definitions/Timestamp" + } + }, + "additionalProperties": false + }, + { + "description": "Never will never expire. Used to express the empty variant", + "type": "object", + "required": [ + "never" + ], + "properties": { + "never": { + "type": "object" + } + }, + "additionalProperties": false + } + ] + }, + "Timestamp": { + "description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```", + "allOf": [ + { + "$ref": "#/definitions/Uint64" + } + ] + }, + "Uint64": { + "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```", + "type": "string" + } + } +} diff --git a/wormhole/cw721-base/schema/contract_info_response.json b/wormhole/cw721-base/schema/contract_info_response.json new file mode 100644 index 00000000..a1671258 --- /dev/null +++ b/wormhole/cw721-base/schema/contract_info_response.json @@ -0,0 +1,17 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ContractInfoResponse", + "type": "object", + "required": [ + "name", + "symbol" + ], + "properties": { + "name": { + "type": "string" + }, + "symbol": { + "type": "string" + } + } +} diff --git a/wormhole/cw721-base/schema/execute_msg.json b/wormhole/cw721-base/schema/execute_msg.json new file mode 100644 index 00000000..5cdf6603 --- /dev/null +++ b/wormhole/cw721-base/schema/execute_msg.json @@ -0,0 +1,310 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ExecuteMsg", + "description": "This is like Cw721ExecuteMsg but we add a Mint command for an owner to make this stand-alone. You will likely want to remove mint and use other control logic in any contract that inherits this.", + "oneOf": [ + { + "description": "Transfer is a base message to move a token to another account without triggering actions", + "type": "object", + "required": [ + "transfer_nft" + ], + "properties": { + "transfer_nft": { + "type": "object", + "required": [ + "recipient", + "token_id" + ], + "properties": { + "recipient": { + "type": "string" + }, + "token_id": { + "type": "string" + } + } + } + }, + "additionalProperties": false + }, + { + "description": "Send is a base message to transfer a token to a contract and trigger an action on the receiving contract.", + "type": "object", + "required": [ + "send_nft" + ], + "properties": { + "send_nft": { + "type": "object", + "required": [ + "contract", + "msg", + "token_id" + ], + "properties": { + "contract": { + "type": "string" + }, + "msg": { + "$ref": "#/definitions/Binary" + }, + "token_id": { + "type": "string" + } + } + } + }, + "additionalProperties": false + }, + { + "description": "Allows operator to transfer / send the token from the owner's account. If expiration is set, then this allowance has a time/height limit", + "type": "object", + "required": [ + "approve" + ], + "properties": { + "approve": { + "type": "object", + "required": [ + "spender", + "token_id" + ], + "properties": { + "expires": { + "anyOf": [ + { + "$ref": "#/definitions/Expiration" + }, + { + "type": "null" + } + ] + }, + "spender": { + "type": "string" + }, + "token_id": { + "type": "string" + } + } + } + }, + "additionalProperties": false + }, + { + "description": "Remove previously granted Approval", + "type": "object", + "required": [ + "revoke" + ], + "properties": { + "revoke": { + "type": "object", + "required": [ + "spender", + "token_id" + ], + "properties": { + "spender": { + "type": "string" + }, + "token_id": { + "type": "string" + } + } + } + }, + "additionalProperties": false + }, + { + "description": "Allows operator to transfer / send any token from the owner's account. If expiration is set, then this allowance has a time/height limit", + "type": "object", + "required": [ + "approve_all" + ], + "properties": { + "approve_all": { + "type": "object", + "required": [ + "operator" + ], + "properties": { + "expires": { + "anyOf": [ + { + "$ref": "#/definitions/Expiration" + }, + { + "type": "null" + } + ] + }, + "operator": { + "type": "string" + } + } + } + }, + "additionalProperties": false + }, + { + "description": "Remove previously granted ApproveAll permission", + "type": "object", + "required": [ + "revoke_all" + ], + "properties": { + "revoke_all": { + "type": "object", + "required": [ + "operator" + ], + "properties": { + "operator": { + "type": "string" + } + } + } + }, + "additionalProperties": false + }, + { + "description": "Mint a new NFT, can only be called by the contract minter", + "type": "object", + "required": [ + "mint" + ], + "properties": { + "mint": { + "$ref": "#/definitions/MintMsg_for_Nullable_Empty" + } + }, + "additionalProperties": false + }, + { + "description": "Burn an NFT the sender has access to", + "type": "object", + "required": [ + "burn" + ], + "properties": { + "burn": { + "type": "object", + "required": [ + "token_id" + ], + "properties": { + "token_id": { + "type": "string" + } + } + } + }, + "additionalProperties": false + } + ], + "definitions": { + "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", + "type": "string" + }, + "Empty": { + "description": "An empty struct that serves as a placeholder in different places, such as contracts that don't set a custom message.\n\nIt is designed to be expressable in correct JSON and JSON Schema but contains no meaningful data. Previously we used enums without cases, but those cannot represented as valid JSON Schema (https://github.com/CosmWasm/cosmwasm/issues/451)", + "type": "object" + }, + "Expiration": { + "description": "Expiration represents a point in time when some event happens. It can compare with a BlockInfo and will return is_expired() == true once the condition is hit (and for every block in the future)", + "oneOf": [ + { + "description": "AtHeight will expire when `env.block.height` >= height", + "type": "object", + "required": [ + "at_height" + ], + "properties": { + "at_height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + { + "description": "AtTime will expire when `env.block.time` >= time", + "type": "object", + "required": [ + "at_time" + ], + "properties": { + "at_time": { + "$ref": "#/definitions/Timestamp" + } + }, + "additionalProperties": false + }, + { + "description": "Never will never expire. Used to express the empty variant", + "type": "object", + "required": [ + "never" + ], + "properties": { + "never": { + "type": "object" + } + }, + "additionalProperties": false + } + ] + }, + "MintMsg_for_Nullable_Empty": { + "type": "object", + "required": [ + "owner", + "token_id" + ], + "properties": { + "extension": { + "description": "Any custom extension used by this contract", + "anyOf": [ + { + "$ref": "#/definitions/Empty" + }, + { + "type": "null" + } + ] + }, + "owner": { + "description": "The owner of the newly minter NFT", + "type": "string" + }, + "token_id": { + "description": "Unique ID of the NFT", + "type": "string" + }, + "token_uri": { + "description": "Universal resource identifier for this NFT Should point to a JSON file that conforms to the ERC721 Metadata JSON Schema", + "type": [ + "string", + "null" + ] + } + } + }, + "Timestamp": { + "description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```", + "allOf": [ + { + "$ref": "#/definitions/Uint64" + } + ] + }, + "Uint64": { + "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```", + "type": "string" + } + } +} diff --git a/wormhole/cw721-base/schema/instantiate_msg.json b/wormhole/cw721-base/schema/instantiate_msg.json new file mode 100644 index 00000000..b024c82c --- /dev/null +++ b/wormhole/cw721-base/schema/instantiate_msg.json @@ -0,0 +1,24 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "InstantiateMsg", + "type": "object", + "required": [ + "minter", + "name", + "symbol" + ], + "properties": { + "minter": { + "description": "The minter is the only one who can create new NFTs. This is designed for a base NFT that is controlled by an external program or contract. You will likely replace this with custom logic in custom NFTs", + "type": "string" + }, + "name": { + "description": "Name of the NFT contract", + "type": "string" + }, + "symbol": { + "description": "Symbol of the NFT contract", + "type": "string" + } + } +} diff --git a/wormhole/cw721-base/schema/minter_response.json b/wormhole/cw721-base/schema/minter_response.json new file mode 100644 index 00000000..a20e0d76 --- /dev/null +++ b/wormhole/cw721-base/schema/minter_response.json @@ -0,0 +1,14 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "MinterResponse", + "description": "Shows who can mint these tokens", + "type": "object", + "required": [ + "minter" + ], + "properties": { + "minter": { + "type": "string" + } + } +} diff --git a/wormhole/cw721-base/schema/nft_info_response.json b/wormhole/cw721-base/schema/nft_info_response.json new file mode 100644 index 00000000..e6bf1d40 --- /dev/null +++ b/wormhole/cw721-base/schema/nft_info_response.json @@ -0,0 +1,31 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "NftInfoResponse", + "type": "object", + "properties": { + "extension": { + "description": "You can add any custom metadata here when you extend cw721-base", + "anyOf": [ + { + "$ref": "#/definitions/Empty" + }, + { + "type": "null" + } + ] + }, + "token_uri": { + "description": "Universal resource identifier for this NFT Should point to a JSON file that conforms to the ERC721 Metadata JSON Schema", + "type": [ + "string", + "null" + ] + } + }, + "definitions": { + "Empty": { + "description": "An empty struct that serves as a placeholder in different places, such as contracts that don't set a custom message.\n\nIt is designed to be expressable in correct JSON and JSON Schema but contains no meaningful data. Previously we used enums without cases, but those cannot represented as valid JSON Schema (https://github.com/CosmWasm/cosmwasm/issues/451)", + "type": "object" + } + } +} diff --git a/wormhole/cw721-base/schema/num_tokens_response.json b/wormhole/cw721-base/schema/num_tokens_response.json new file mode 100644 index 00000000..4647c23a --- /dev/null +++ b/wormhole/cw721-base/schema/num_tokens_response.json @@ -0,0 +1,15 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "NumTokensResponse", + "type": "object", + "required": [ + "count" + ], + "properties": { + "count": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + } +} diff --git a/wormhole/cw721-base/schema/operators_response.json b/wormhole/cw721-base/schema/operators_response.json new file mode 100644 index 00000000..53703072 --- /dev/null +++ b/wormhole/cw721-base/schema/operators_response.json @@ -0,0 +1,97 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "OperatorsResponse", + "type": "object", + "required": [ + "operators" + ], + "properties": { + "operators": { + "type": "array", + "items": { + "$ref": "#/definitions/Approval" + } + } + }, + "definitions": { + "Approval": { + "type": "object", + "required": [ + "expires", + "spender" + ], + "properties": { + "expires": { + "description": "When the Approval expires (maybe Expiration::never)", + "allOf": [ + { + "$ref": "#/definitions/Expiration" + } + ] + }, + "spender": { + "description": "Account that can transfer/send the token", + "type": "string" + } + } + }, + "Expiration": { + "description": "Expiration represents a point in time when some event happens. It can compare with a BlockInfo and will return is_expired() == true once the condition is hit (and for every block in the future)", + "oneOf": [ + { + "description": "AtHeight will expire when `env.block.height` >= height", + "type": "object", + "required": [ + "at_height" + ], + "properties": { + "at_height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + { + "description": "AtTime will expire when `env.block.time` >= time", + "type": "object", + "required": [ + "at_time" + ], + "properties": { + "at_time": { + "$ref": "#/definitions/Timestamp" + } + }, + "additionalProperties": false + }, + { + "description": "Never will never expire. Used to express the empty variant", + "type": "object", + "required": [ + "never" + ], + "properties": { + "never": { + "type": "object" + } + }, + "additionalProperties": false + } + ] + }, + "Timestamp": { + "description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```", + "allOf": [ + { + "$ref": "#/definitions/Uint64" + } + ] + }, + "Uint64": { + "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```", + "type": "string" + } + } +} diff --git a/wormhole/cw721-base/schema/owner_of_response.json b/wormhole/cw721-base/schema/owner_of_response.json new file mode 100644 index 00000000..1258d671 --- /dev/null +++ b/wormhole/cw721-base/schema/owner_of_response.json @@ -0,0 +1,103 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "OwnerOfResponse", + "type": "object", + "required": [ + "approvals", + "owner" + ], + "properties": { + "approvals": { + "description": "If set this address is approved to transfer/send the token as well", + "type": "array", + "items": { + "$ref": "#/definitions/Approval" + } + }, + "owner": { + "description": "Owner of the token", + "type": "string" + } + }, + "definitions": { + "Approval": { + "type": "object", + "required": [ + "expires", + "spender" + ], + "properties": { + "expires": { + "description": "When the Approval expires (maybe Expiration::never)", + "allOf": [ + { + "$ref": "#/definitions/Expiration" + } + ] + }, + "spender": { + "description": "Account that can transfer/send the token", + "type": "string" + } + } + }, + "Expiration": { + "description": "Expiration represents a point in time when some event happens. It can compare with a BlockInfo and will return is_expired() == true once the condition is hit (and for every block in the future)", + "oneOf": [ + { + "description": "AtHeight will expire when `env.block.height` >= height", + "type": "object", + "required": [ + "at_height" + ], + "properties": { + "at_height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + { + "description": "AtTime will expire when `env.block.time` >= time", + "type": "object", + "required": [ + "at_time" + ], + "properties": { + "at_time": { + "$ref": "#/definitions/Timestamp" + } + }, + "additionalProperties": false + }, + { + "description": "Never will never expire. Used to express the empty variant", + "type": "object", + "required": [ + "never" + ], + "properties": { + "never": { + "type": "object" + } + }, + "additionalProperties": false + } + ] + }, + "Timestamp": { + "description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```", + "allOf": [ + { + "$ref": "#/definitions/Uint64" + } + ] + }, + "Uint64": { + "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```", + "type": "string" + } + } +} diff --git a/wormhole/cw721-base/schema/query_msg.json b/wormhole/cw721-base/schema/query_msg.json new file mode 100644 index 00000000..cd3a9561 --- /dev/null +++ b/wormhole/cw721-base/schema/query_msg.json @@ -0,0 +1,285 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "QueryMsg", + "oneOf": [ + { + "description": "Return the owner of the given token, error if token does not exist Return type: OwnerOfResponse", + "type": "object", + "required": [ + "owner_of" + ], + "properties": { + "owner_of": { + "type": "object", + "required": [ + "token_id" + ], + "properties": { + "include_expired": { + "description": "unset or false will filter out expired approvals, you must set to true to see them", + "type": [ + "boolean", + "null" + ] + }, + "token_id": { + "type": "string" + } + } + } + }, + "additionalProperties": false + }, + { + "description": "Return operator that can access all of the owner's tokens. Return type: `ApprovalResponse`", + "type": "object", + "required": [ + "approval" + ], + "properties": { + "approval": { + "type": "object", + "required": [ + "spender", + "token_id" + ], + "properties": { + "include_expired": { + "type": [ + "boolean", + "null" + ] + }, + "spender": { + "type": "string" + }, + "token_id": { + "type": "string" + } + } + } + }, + "additionalProperties": false + }, + { + "description": "Return approvals that a token has Return type: `ApprovalsResponse`", + "type": "object", + "required": [ + "approvals" + ], + "properties": { + "approvals": { + "type": "object", + "required": [ + "token_id" + ], + "properties": { + "include_expired": { + "type": [ + "boolean", + "null" + ] + }, + "token_id": { + "type": "string" + } + } + } + }, + "additionalProperties": false + }, + { + "description": "List all operators that can access all of the owner's tokens Return type: `OperatorsResponse`", + "type": "object", + "required": [ + "all_operators" + ], + "properties": { + "all_operators": { + "type": "object", + "required": [ + "owner" + ], + "properties": { + "include_expired": { + "description": "unset or false will filter out expired items, you must set to true to see them", + "type": [ + "boolean", + "null" + ] + }, + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "owner": { + "type": "string" + }, + "start_after": { + "type": [ + "string", + "null" + ] + } + } + } + }, + "additionalProperties": false + }, + { + "description": "Total number of tokens issued", + "type": "object", + "required": [ + "num_tokens" + ], + "properties": { + "num_tokens": { + "type": "object" + } + }, + "additionalProperties": false + }, + { + "description": "With MetaData Extension. Returns top-level metadata about the contract: `ContractInfoResponse`", + "type": "object", + "required": [ + "contract_info" + ], + "properties": { + "contract_info": { + "type": "object" + } + }, + "additionalProperties": false + }, + { + "description": "With MetaData Extension. Returns metadata about one particular token, based on *ERC721 Metadata JSON Schema* but directly from the contract: `NftInfoResponse`", + "type": "object", + "required": [ + "nft_info" + ], + "properties": { + "nft_info": { + "type": "object", + "required": [ + "token_id" + ], + "properties": { + "token_id": { + "type": "string" + } + } + } + }, + "additionalProperties": false + }, + { + "description": "With MetaData Extension. Returns the result of both `NftInfo` and `OwnerOf` as one query as an optimization for clients: `AllNftInfo`", + "type": "object", + "required": [ + "all_nft_info" + ], + "properties": { + "all_nft_info": { + "type": "object", + "required": [ + "token_id" + ], + "properties": { + "include_expired": { + "description": "unset or false will filter out expired approvals, you must set to true to see them", + "type": [ + "boolean", + "null" + ] + }, + "token_id": { + "type": "string" + } + } + } + }, + "additionalProperties": false + }, + { + "description": "With Enumerable extension. Returns all tokens owned by the given address, [] if unset. Return type: TokensResponse.", + "type": "object", + "required": [ + "tokens" + ], + "properties": { + "tokens": { + "type": "object", + "required": [ + "owner" + ], + "properties": { + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "owner": { + "type": "string" + }, + "start_after": { + "type": [ + "string", + "null" + ] + } + } + } + }, + "additionalProperties": false + }, + { + "description": "With Enumerable extension. Requires pagination. Lists all token_ids controlled by the contract. Return type: TokensResponse.", + "type": "object", + "required": [ + "all_tokens" + ], + "properties": { + "all_tokens": { + "type": "object", + "properties": { + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "type": [ + "string", + "null" + ] + } + } + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "minter" + ], + "properties": { + "minter": { + "type": "object" + } + }, + "additionalProperties": false + } + ] +} diff --git a/wormhole/cw721-base/schema/tokens_response.json b/wormhole/cw721-base/schema/tokens_response.json new file mode 100644 index 00000000..b8e3d75b --- /dev/null +++ b/wormhole/cw721-base/schema/tokens_response.json @@ -0,0 +1,17 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "TokensResponse", + "type": "object", + "required": [ + "tokens" + ], + "properties": { + "tokens": { + "description": "Contains all token_ids in lexicographical ordering If there are more than `limit`, use `start_from` in future queries to achieve pagination.", + "type": "array", + "items": { + "type": "string" + } + } + } +} diff --git a/wormhole/cw721-base/src/contract_tests.rs b/wormhole/cw721-base/src/contract_tests.rs new file mode 100644 index 00000000..646f4637 --- /dev/null +++ b/wormhole/cw721-base/src/contract_tests.rs @@ -0,0 +1,731 @@ +#![cfg(test)] +use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info}; +use cosmwasm_std::{from_binary, to_binary, CosmosMsg, DepsMut, Empty, Response, WasmMsg}; + +use cw721::{ + Approval, ApprovalResponse, ContractInfoResponse, Cw721Query, Cw721ReceiveMsg, Expiration, + NftInfoResponse, OperatorsResponse, OwnerOfResponse, +}; + +use crate::{ + ContractError, Cw721Contract, ExecuteMsg, Extension, InstantiateMsg, MintMsg, QueryMsg, +}; + +const MINTER: &str = "merlin"; +const CONTRACT_NAME: &str = "Magic Power"; +const SYMBOL: &str = "MGK"; + +fn setup_contract(deps: DepsMut<'_>) -> Cw721Contract<'static, Extension, Empty> { + let contract = Cw721Contract::default(); + let msg = InstantiateMsg { + name: CONTRACT_NAME.to_string(), + symbol: SYMBOL.to_string(), + minter: String::from(MINTER), + }; + let info = mock_info("creator", &[]); + let res = contract.instantiate(deps, mock_env(), info, msg).unwrap(); + assert_eq!(0, res.messages.len()); + contract +} + +#[test] +fn proper_instantiation() { + let mut deps = mock_dependencies(); + let contract = Cw721Contract::::default(); + + let msg = InstantiateMsg { + name: CONTRACT_NAME.to_string(), + symbol: SYMBOL.to_string(), + minter: String::from(MINTER), + }; + let info = mock_info("creator", &[]); + + // we can just call .unwrap() to assert this was a success + let res = contract + .instantiate(deps.as_mut(), mock_env(), info, msg) + .unwrap(); + assert_eq!(0, res.messages.len()); + + // it worked, let's query the state + let res = contract.minter(deps.as_ref()).unwrap(); + assert_eq!(MINTER, res.minter); + let info = contract.contract_info(deps.as_ref()).unwrap(); + assert_eq!( + info, + ContractInfoResponse { + name: CONTRACT_NAME.to_string(), + symbol: SYMBOL.to_string(), + } + ); + + let count = contract.num_tokens(deps.as_ref()).unwrap(); + assert_eq!(0, count.count); + + // list the token_ids + let tokens = contract.all_tokens(deps.as_ref(), None, None).unwrap(); + assert_eq!(0, tokens.tokens.len()); +} + +#[test] +fn minting() { + let mut deps = mock_dependencies(); + let contract = setup_contract(deps.as_mut()); + + let token_id = "petrify".to_string(); + let token_uri = "https://www.merriam-webster.com/dictionary/petrify".to_string(); + + let mint_msg = ExecuteMsg::Mint(MintMsg:: { + token_id: token_id.clone(), + owner: String::from("medusa"), + token_uri: Some(token_uri.clone()), + extension: None, + }); + + // random cannot mint + let random = mock_info("random", &[]); + let err = contract + .execute(deps.as_mut(), mock_env(), random, mint_msg.clone()) + .unwrap_err(); + assert_eq!(err, ContractError::Unauthorized {}); + + // minter can mint + let allowed = mock_info(MINTER, &[]); + let _ = contract + .execute(deps.as_mut(), mock_env(), allowed, mint_msg) + .unwrap(); + + // ensure num tokens increases + let count = contract.num_tokens(deps.as_ref()).unwrap(); + assert_eq!(1, count.count); + + // unknown nft returns error + let _ = contract + .nft_info(deps.as_ref(), "unknown".to_string()) + .unwrap_err(); + + // this nft info is correct + let info = contract.nft_info(deps.as_ref(), token_id.clone()).unwrap(); + assert_eq!( + info, + NftInfoResponse:: { + token_uri: Some(token_uri), + extension: None, + } + ); + + // owner info is correct + let owner = contract + .owner_of(deps.as_ref(), mock_env(), token_id.clone(), true) + .unwrap(); + assert_eq!( + owner, + OwnerOfResponse { + owner: String::from("medusa"), + approvals: vec![], + } + ); + + // Cannot mint same token_id again + let mint_msg2 = ExecuteMsg::Mint(MintMsg:: { + token_id: token_id.clone(), + owner: String::from("hercules"), + token_uri: None, + extension: None, + }); + + let allowed = mock_info(MINTER, &[]); + let err = contract + .execute(deps.as_mut(), mock_env(), allowed, mint_msg2) + .unwrap_err(); + assert_eq!(err, ContractError::Claimed {}); + + // list the token_ids + let tokens = contract.all_tokens(deps.as_ref(), None, None).unwrap(); + assert_eq!(1, tokens.tokens.len()); + assert_eq!(vec![token_id], tokens.tokens); +} + +#[test] +fn burning() { + let mut deps = mock_dependencies(); + let contract = setup_contract(deps.as_mut()); + + let token_id = "petrify".to_string(); + let token_uri = "https://www.merriam-webster.com/dictionary/petrify".to_string(); + + let mint_msg = ExecuteMsg::Mint(MintMsg:: { + token_id: token_id.clone(), + owner: MINTER.to_string(), + token_uri: Some(token_uri), + extension: None, + }); + + let burn_msg = ExecuteMsg::Burn { token_id }; + + // mint some NFT + let allowed = mock_info(MINTER, &[]); + let _ = contract + .execute(deps.as_mut(), mock_env(), allowed.clone(), mint_msg) + .unwrap(); + + // random not allowed to burn + let random = mock_info("random", &[]); + let err = contract + .execute(deps.as_mut(), mock_env(), random, burn_msg.clone()) + .unwrap_err(); + + assert_eq!(err, ContractError::Unauthorized {}); + + let _ = contract + .execute(deps.as_mut(), mock_env(), allowed, burn_msg) + .unwrap(); + + // ensure num tokens decreases + let count = contract.num_tokens(deps.as_ref()).unwrap(); + assert_eq!(0, count.count); + + // trying to get nft returns error + let _ = contract + .nft_info(deps.as_ref(), "petrify".to_string()) + .unwrap_err(); + + // list the token_ids + let tokens = contract.all_tokens(deps.as_ref(), None, None).unwrap(); + assert!(tokens.tokens.is_empty()); +} + +#[test] +fn transferring_nft() { + let mut deps = mock_dependencies(); + let contract = setup_contract(deps.as_mut()); + + // Mint a token + let token_id = "melt".to_string(); + let token_uri = "https://www.merriam-webster.com/dictionary/melt".to_string(); + + let mint_msg = ExecuteMsg::Mint(MintMsg:: { + token_id: token_id.clone(), + owner: String::from("venus"), + token_uri: Some(token_uri), + extension: None, + }); + + let minter = mock_info(MINTER, &[]); + contract + .execute(deps.as_mut(), mock_env(), minter, mint_msg) + .unwrap(); + + // random cannot transfer + let random = mock_info("random", &[]); + let transfer_msg = ExecuteMsg::TransferNft { + recipient: String::from("random"), + token_id: token_id.clone(), + }; + + let err = contract + .execute(deps.as_mut(), mock_env(), random, transfer_msg) + .unwrap_err(); + assert_eq!(err, ContractError::Unauthorized {}); + + // owner can + let random = mock_info("venus", &[]); + let transfer_msg = ExecuteMsg::TransferNft { + recipient: String::from("random"), + token_id: token_id.clone(), + }; + + let res = contract + .execute(deps.as_mut(), mock_env(), random, transfer_msg) + .unwrap(); + + assert_eq!( + res, + Response::new() + .add_attribute("action", "transfer_nft") + .add_attribute("sender", "venus") + .add_attribute("recipient", "random") + .add_attribute("token_id", token_id) + ); +} + +#[test] +fn sending_nft() { + let mut deps = mock_dependencies(); + let contract = setup_contract(deps.as_mut()); + + // Mint a token + let token_id = "melt".to_string(); + let token_uri = "https://www.merriam-webster.com/dictionary/melt".to_string(); + + let mint_msg = ExecuteMsg::Mint(MintMsg:: { + token_id: token_id.clone(), + owner: String::from("venus"), + token_uri: Some(token_uri), + extension: None, + }); + + let minter = mock_info(MINTER, &[]); + contract + .execute(deps.as_mut(), mock_env(), minter, mint_msg) + .unwrap(); + + let msg = to_binary("You now have the melting power").unwrap(); + let target = String::from("another_contract"); + let send_msg = ExecuteMsg::SendNft { + contract: target.clone(), + token_id: token_id.clone(), + msg: msg.clone(), + }; + + let random = mock_info("random", &[]); + let err = contract + .execute(deps.as_mut(), mock_env(), random, send_msg.clone()) + .unwrap_err(); + assert_eq!(err, ContractError::Unauthorized {}); + + // but owner can + let random = mock_info("venus", &[]); + let res = contract + .execute(deps.as_mut(), mock_env(), random, send_msg) + .unwrap(); + + let payload = Cw721ReceiveMsg { + sender: String::from("venus"), + token_id: token_id.clone(), + msg, + }; + let expected = payload.into_cosmos_msg(target.clone()).unwrap(); + // ensure expected serializes as we think it should + match &expected { + CosmosMsg::Wasm(WasmMsg::Execute { contract_addr, .. }) => { + assert_eq!(contract_addr, &target) + } + m => panic!("Unexpected message type: {:?}", m), + } + // and make sure this is the request sent by the contract + assert_eq!( + res, + Response::new() + .add_message(expected) + .add_attribute("action", "send_nft") + .add_attribute("sender", "venus") + .add_attribute("recipient", "another_contract") + .add_attribute("token_id", token_id) + ); +} + +#[test] +fn approving_revoking() { + let mut deps = mock_dependencies(); + let contract = setup_contract(deps.as_mut()); + + // Mint a token + let token_id = "grow".to_string(); + let token_uri = "https://www.merriam-webster.com/dictionary/grow".to_string(); + + let mint_msg = ExecuteMsg::Mint(MintMsg:: { + token_id: token_id.clone(), + owner: String::from("demeter"), + token_uri: Some(token_uri), + extension: None, + }); + + let minter = mock_info(MINTER, &[]); + contract + .execute(deps.as_mut(), mock_env(), minter, mint_msg) + .unwrap(); + + // Give random transferring power + let approve_msg = ExecuteMsg::Approve { + spender: String::from("random"), + token_id: token_id.clone(), + expires: None, + }; + let owner = mock_info("demeter", &[]); + let res = contract + .execute(deps.as_mut(), mock_env(), owner, approve_msg) + .unwrap(); + assert_eq!( + res, + Response::new() + .add_attribute("action", "approve") + .add_attribute("sender", "demeter") + .add_attribute("spender", "random") + .add_attribute("token_id", token_id.clone()) + ); + + // test approval query + let res = contract + .approval( + deps.as_ref(), + mock_env(), + token_id.clone(), + String::from("random"), + true, + ) + .unwrap(); + assert_eq!( + res, + ApprovalResponse { + approval: Approval { + spender: String::from("random"), + expires: Expiration::Never {} + } + } + ); + + // random can now transfer + let random = mock_info("random", &[]); + let transfer_msg = ExecuteMsg::TransferNft { + recipient: String::from("person"), + token_id: token_id.clone(), + }; + contract + .execute(deps.as_mut(), mock_env(), random, transfer_msg) + .unwrap(); + + // Approvals are removed / cleared + let query_msg = QueryMsg::OwnerOf { + token_id: token_id.clone(), + include_expired: None, + }; + let res: OwnerOfResponse = from_binary( + &contract + .query(deps.as_ref(), mock_env(), query_msg.clone()) + .unwrap(), + ) + .unwrap(); + assert_eq!( + res, + OwnerOfResponse { + owner: String::from("person"), + approvals: vec![], + } + ); + + // Approve, revoke, and check for empty, to test revoke + let approve_msg = ExecuteMsg::Approve { + spender: String::from("random"), + token_id: token_id.clone(), + expires: None, + }; + let owner = mock_info("person", &[]); + contract + .execute(deps.as_mut(), mock_env(), owner.clone(), approve_msg) + .unwrap(); + + let revoke_msg = ExecuteMsg::Revoke { + spender: String::from("random"), + token_id, + }; + contract + .execute(deps.as_mut(), mock_env(), owner, revoke_msg) + .unwrap(); + + // Approvals are now removed / cleared + let res: OwnerOfResponse = from_binary( + &contract + .query(deps.as_ref(), mock_env(), query_msg) + .unwrap(), + ) + .unwrap(); + assert_eq!( + res, + OwnerOfResponse { + owner: String::from("person"), + approvals: vec![], + } + ); +} + +#[test] +fn approving_all_revoking_all() { + let mut deps = mock_dependencies(); + let contract = setup_contract(deps.as_mut()); + + // Mint a couple tokens (from the same owner) + let token_id1 = "grow1".to_string(); + let token_uri1 = "https://www.merriam-webster.com/dictionary/grow1".to_string(); + + let token_id2 = "grow2".to_string(); + let token_uri2 = "https://www.merriam-webster.com/dictionary/grow2".to_string(); + + let mint_msg1 = ExecuteMsg::Mint(MintMsg:: { + token_id: token_id1.clone(), + owner: String::from("demeter"), + token_uri: Some(token_uri1), + extension: None, + }); + + let minter = mock_info(MINTER, &[]); + contract + .execute(deps.as_mut(), mock_env(), minter.clone(), mint_msg1) + .unwrap(); + + let mint_msg2 = ExecuteMsg::Mint(MintMsg:: { + token_id: token_id2.clone(), + owner: String::from("demeter"), + token_uri: Some(token_uri2), + extension: None, + }); + + contract + .execute(deps.as_mut(), mock_env(), minter, mint_msg2) + .unwrap(); + + // paginate the token_ids + let tokens = contract.all_tokens(deps.as_ref(), None, Some(1)).unwrap(); + assert_eq!(1, tokens.tokens.len()); + assert_eq!(vec![token_id1.clone()], tokens.tokens); + let tokens = contract + .all_tokens(deps.as_ref(), Some(token_id1.clone()), Some(3)) + .unwrap(); + assert_eq!(1, tokens.tokens.len()); + assert_eq!(vec![token_id2.clone()], tokens.tokens); + + // demeter gives random full (operator) power over her tokens + let approve_all_msg = ExecuteMsg::ApproveAll { + operator: String::from("random"), + expires: None, + }; + let owner = mock_info("demeter", &[]); + let res = contract + .execute(deps.as_mut(), mock_env(), owner, approve_all_msg) + .unwrap(); + assert_eq!( + res, + Response::new() + .add_attribute("action", "approve_all") + .add_attribute("sender", "demeter") + .add_attribute("operator", "random") + ); + + // random can now transfer + let random = mock_info("random", &[]); + let transfer_msg = ExecuteMsg::TransferNft { + recipient: String::from("person"), + token_id: token_id1, + }; + contract + .execute(deps.as_mut(), mock_env(), random.clone(), transfer_msg) + .unwrap(); + + // random can now send + let inner_msg = WasmMsg::Execute { + contract_addr: "another_contract".into(), + msg: to_binary("You now also have the growing power").unwrap(), + funds: vec![], + }; + let msg: CosmosMsg = CosmosMsg::Wasm(inner_msg); + + let send_msg = ExecuteMsg::SendNft { + contract: String::from("another_contract"), + token_id: token_id2, + msg: to_binary(&msg).unwrap(), + }; + contract + .execute(deps.as_mut(), mock_env(), random, send_msg) + .unwrap(); + + // Approve_all, revoke_all, and check for empty, to test revoke_all + let approve_all_msg = ExecuteMsg::ApproveAll { + operator: String::from("operator"), + expires: None, + }; + // person is now the owner of the tokens + let owner = mock_info("person", &[]); + contract + .execute(deps.as_mut(), mock_env(), owner, approve_all_msg) + .unwrap(); + + let res = contract + .operators( + deps.as_ref(), + mock_env(), + String::from("person"), + true, + None, + None, + ) + .unwrap(); + assert_eq!( + res, + OperatorsResponse { + operators: vec![cw721::Approval { + spender: String::from("operator"), + expires: Expiration::Never {} + }] + } + ); + + // second approval + let buddy_expires = Expiration::AtHeight(1234567); + let approve_all_msg = ExecuteMsg::ApproveAll { + operator: String::from("buddy"), + expires: Some(buddy_expires), + }; + let owner = mock_info("person", &[]); + contract + .execute(deps.as_mut(), mock_env(), owner.clone(), approve_all_msg) + .unwrap(); + + // and paginate queries + let res = contract + .operators( + deps.as_ref(), + mock_env(), + String::from("person"), + true, + None, + Some(1), + ) + .unwrap(); + assert_eq!( + res, + OperatorsResponse { + operators: vec![cw721::Approval { + spender: String::from("buddy"), + expires: buddy_expires, + }] + } + ); + let res = contract + .operators( + deps.as_ref(), + mock_env(), + String::from("person"), + true, + Some(String::from("buddy")), + Some(2), + ) + .unwrap(); + assert_eq!( + res, + OperatorsResponse { + operators: vec![cw721::Approval { + spender: String::from("operator"), + expires: Expiration::Never {} + }] + } + ); + + let revoke_all_msg = ExecuteMsg::RevokeAll { + operator: String::from("operator"), + }; + contract + .execute(deps.as_mut(), mock_env(), owner, revoke_all_msg) + .unwrap(); + + // Approvals are removed / cleared without affecting others + let res = contract + .operators( + deps.as_ref(), + mock_env(), + String::from("person"), + false, + None, + None, + ) + .unwrap(); + assert_eq!( + res, + OperatorsResponse { + operators: vec![cw721::Approval { + spender: String::from("buddy"), + expires: buddy_expires, + }] + } + ); + + // ensure the filter works (nothing should be here + let mut late_env = mock_env(); + late_env.block.height = 1234568; //expired + let res = contract + .operators( + deps.as_ref(), + late_env, + String::from("person"), + false, + None, + None, + ) + .unwrap(); + assert_eq!(0, res.operators.len()); +} + +#[test] +fn query_tokens_by_owner() { + let mut deps = mock_dependencies(); + let contract = setup_contract(deps.as_mut()); + let minter = mock_info(MINTER, &[]); + + // Mint a couple tokens (from the same owner) + let token_id1 = "grow1".to_string(); + let demeter = String::from("demeter"); + let token_id2 = "grow2".to_string(); + let ceres = String::from("ceres"); + let token_id3 = "sing".to_string(); + + let mint_msg = ExecuteMsg::Mint(MintMsg:: { + token_id: token_id1.clone(), + owner: demeter.clone(), + token_uri: None, + extension: None, + }); + contract + .execute(deps.as_mut(), mock_env(), minter.clone(), mint_msg) + .unwrap(); + + let mint_msg = ExecuteMsg::Mint(MintMsg:: { + token_id: token_id2.clone(), + owner: ceres.clone(), + token_uri: None, + extension: None, + }); + contract + .execute(deps.as_mut(), mock_env(), minter.clone(), mint_msg) + .unwrap(); + + let mint_msg = ExecuteMsg::Mint(MintMsg:: { + token_id: token_id3.clone(), + owner: demeter.clone(), + token_uri: None, + extension: None, + }); + contract + .execute(deps.as_mut(), mock_env(), minter, mint_msg) + .unwrap(); + + // get all tokens in order: + let expected = vec![token_id1.clone(), token_id2.clone(), token_id3.clone()]; + let tokens = contract.all_tokens(deps.as_ref(), None, None).unwrap(); + assert_eq!(&expected, &tokens.tokens); + // paginate + let tokens = contract.all_tokens(deps.as_ref(), None, Some(2)).unwrap(); + assert_eq!(&expected[..2], &tokens.tokens[..]); + let tokens = contract + .all_tokens(deps.as_ref(), Some(expected[1].clone()), None) + .unwrap(); + assert_eq!(&expected[2..], &tokens.tokens[..]); + + // get by owner + let by_ceres = vec![token_id2]; + let by_demeter = vec![token_id1, token_id3]; + // all tokens by owner + let tokens = contract + .tokens(deps.as_ref(), demeter.clone(), None, None) + .unwrap(); + assert_eq!(&by_demeter, &tokens.tokens); + let tokens = contract.tokens(deps.as_ref(), ceres, None, None).unwrap(); + assert_eq!(&by_ceres, &tokens.tokens); + + // paginate for demeter + let tokens = contract + .tokens(deps.as_ref(), demeter.clone(), None, Some(1)) + .unwrap(); + assert_eq!(&by_demeter[..1], &tokens.tokens[..]); + let tokens = contract + .tokens(deps.as_ref(), demeter, Some(by_demeter[0].clone()), Some(3)) + .unwrap(); + assert_eq!(&by_demeter[1..], &tokens.tokens[..]); +} diff --git a/wormhole/cw721-base/src/error.rs b/wormhole/cw721-base/src/error.rs new file mode 100644 index 00000000..db1ceaad --- /dev/null +++ b/wormhole/cw721-base/src/error.rs @@ -0,0 +1,20 @@ +use cosmwasm_std::StdError; +use thiserror::Error; + +#[derive(Error, Debug, PartialEq)] +pub enum ContractError { + #[error("{0}")] + Std(#[from] StdError), + + #[error("Unauthorized")] + Unauthorized {}, + + #[error("token_id already claimed")] + Claimed {}, + + #[error("Cannot set approval that is already expired")] + Expired {}, + + #[error("Approval not found for: {spender}")] + ApprovalNotFound { spender: String }, +} diff --git a/wormhole/cw721-base/src/execute.rs b/wormhole/cw721-base/src/execute.rs new file mode 100644 index 00000000..66d88e76 --- /dev/null +++ b/wormhole/cw721-base/src/execute.rs @@ -0,0 +1,397 @@ +use serde::de::DeserializeOwned; +use serde::Serialize; + +use cosmwasm_std::{Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult}; + +use cw2::set_contract_version; +use cw721::{ContractInfoResponse, CustomMsg, Cw721Execute, Cw721ReceiveMsg, Expiration}; + +use crate::error::ContractError; +use crate::msg::{ExecuteMsg, InstantiateMsg, MintMsg}; +use crate::state::{Approval, Cw721Contract, TokenInfo}; + +// version info for migration info +const CONTRACT_NAME: &str = "crates.io:cw721-base"; +const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); + +impl<'a, T, C> Cw721Contract<'a, T, C> +where + T: Serialize + DeserializeOwned + Clone, + C: CustomMsg, +{ + pub fn instantiate( + &self, + deps: DepsMut, + _env: Env, + _info: MessageInfo, + msg: InstantiateMsg, + ) -> StdResult> { + set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + + let info = ContractInfoResponse { + name: msg.name, + symbol: msg.symbol, + }; + self.contract_info.save(deps.storage, &info)?; + let minter = deps.api.addr_validate(&msg.minter)?; + self.minter.save(deps.storage, &minter)?; + Ok(Response::default()) + } + + pub fn execute( + &self, + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: ExecuteMsg, + ) -> Result, ContractError> { + match msg { + ExecuteMsg::Mint(msg) => self.mint(deps, env, info, msg), + ExecuteMsg::Approve { + spender, + token_id, + expires, + } => self.approve(deps, env, info, spender, token_id, expires), + ExecuteMsg::Revoke { spender, token_id } => { + self.revoke(deps, env, info, spender, token_id) + } + ExecuteMsg::ApproveAll { operator, expires } => { + self.approve_all(deps, env, info, operator, expires) + } + ExecuteMsg::RevokeAll { operator } => self.revoke_all(deps, env, info, operator), + ExecuteMsg::TransferNft { + recipient, + token_id, + } => self.transfer_nft(deps, env, info, recipient, token_id), + ExecuteMsg::SendNft { + contract, + token_id, + msg, + } => self.send_nft(deps, env, info, contract, token_id, msg), + ExecuteMsg::Burn { token_id } => self.burn(deps, env, info, token_id), + } + } +} + +// TODO pull this into some sort of trait extension?? +impl<'a, T, C> Cw721Contract<'a, T, C> +where + T: Serialize + DeserializeOwned + Clone, + C: CustomMsg, +{ + pub fn mint( + &self, + deps: DepsMut, + _env: Env, + info: MessageInfo, + msg: MintMsg, + ) -> Result, ContractError> { + let minter = self.minter.load(deps.storage)?; + + if info.sender != minter { + return Err(ContractError::Unauthorized {}); + } + + // create the token + let token = TokenInfo { + owner: deps.api.addr_validate(&msg.owner)?, + approvals: vec![], + token_uri: msg.token_uri, + extension: msg.extension, + }; + self.tokens + .update(deps.storage, &msg.token_id, |old| match old { + Some(_) => Err(ContractError::Claimed {}), + None => Ok(token), + })?; + + self.increment_tokens(deps.storage)?; + + Ok(Response::new() + .add_attribute("action", "mint") + .add_attribute("minter", info.sender) + .add_attribute("token_id", msg.token_id)) + } +} + +impl<'a, T, C> Cw721Execute for Cw721Contract<'a, T, C> +where + T: Serialize + DeserializeOwned + Clone, + C: CustomMsg, +{ + type Err = ContractError; + + fn transfer_nft( + &self, + deps: DepsMut, + env: Env, + info: MessageInfo, + recipient: String, + token_id: String, + ) -> Result, ContractError> { + self._transfer_nft(deps, &env, &info, &recipient, &token_id)?; + + Ok(Response::new() + .add_attribute("action", "transfer_nft") + .add_attribute("sender", info.sender) + .add_attribute("recipient", recipient) + .add_attribute("token_id", token_id)) + } + + fn send_nft( + &self, + deps: DepsMut, + env: Env, + info: MessageInfo, + contract: String, + token_id: String, + msg: Binary, + ) -> Result, ContractError> { + // Transfer token + self._transfer_nft(deps, &env, &info, &contract, &token_id)?; + + let send = Cw721ReceiveMsg { + sender: info.sender.to_string(), + token_id: token_id.clone(), + msg, + }; + + // Send message + Ok(Response::new() + .add_message(send.into_cosmos_msg(contract.clone())?) + .add_attribute("action", "send_nft") + .add_attribute("sender", info.sender) + .add_attribute("recipient", contract) + .add_attribute("token_id", token_id)) + } + + fn approve( + &self, + deps: DepsMut, + env: Env, + info: MessageInfo, + spender: String, + token_id: String, + expires: Option, + ) -> Result, ContractError> { + self._update_approvals(deps, &env, &info, &spender, &token_id, true, expires)?; + + Ok(Response::new() + .add_attribute("action", "approve") + .add_attribute("sender", info.sender) + .add_attribute("spender", spender) + .add_attribute("token_id", token_id)) + } + + fn revoke( + &self, + deps: DepsMut, + env: Env, + info: MessageInfo, + spender: String, + token_id: String, + ) -> Result, ContractError> { + self._update_approvals(deps, &env, &info, &spender, &token_id, false, None)?; + + Ok(Response::new() + .add_attribute("action", "revoke") + .add_attribute("sender", info.sender) + .add_attribute("spender", spender) + .add_attribute("token_id", token_id)) + } + + fn approve_all( + &self, + deps: DepsMut, + env: Env, + info: MessageInfo, + operator: String, + expires: Option, + ) -> Result, ContractError> { + // reject expired data as invalid + let expires = expires.unwrap_or_default(); + if expires.is_expired(&env.block) { + return Err(ContractError::Expired {}); + } + + // set the operator for us + let operator_addr = deps.api.addr_validate(&operator)?; + self.operators + .save(deps.storage, (&info.sender, &operator_addr), &expires)?; + + Ok(Response::new() + .add_attribute("action", "approve_all") + .add_attribute("sender", info.sender) + .add_attribute("operator", operator)) + } + + fn revoke_all( + &self, + deps: DepsMut, + _env: Env, + info: MessageInfo, + operator: String, + ) -> Result, ContractError> { + let operator_addr = deps.api.addr_validate(&operator)?; + self.operators + .remove(deps.storage, (&info.sender, &operator_addr)); + + Ok(Response::new() + .add_attribute("action", "revoke_all") + .add_attribute("sender", info.sender) + .add_attribute("operator", operator)) + } + + fn burn( + &self, + deps: DepsMut, + env: Env, + info: MessageInfo, + token_id: String, + ) -> Result, ContractError> { + let token = self.tokens.load(deps.storage, &token_id)?; + self.check_can_send(deps.as_ref(), &env, &info, &token)?; + + self.tokens.remove(deps.storage, &token_id)?; + self.decrement_tokens(deps.storage)?; + + Ok(Response::new() + .add_attribute("action", "burn") + .add_attribute("sender", info.sender) + .add_attribute("token_id", token_id)) + } +} + +// helpers +impl<'a, T, C> Cw721Contract<'a, T, C> +where + T: Serialize + DeserializeOwned + Clone, + C: CustomMsg, +{ + pub fn _transfer_nft( + &self, + deps: DepsMut, + env: &Env, + info: &MessageInfo, + recipient: &str, + token_id: &str, + ) -> Result, ContractError> { + let mut token = self.tokens.load(deps.storage, token_id)?; + // ensure we have permissions + self.check_can_send(deps.as_ref(), env, info, &token)?; + // set owner and remove existing approvals + token.owner = deps.api.addr_validate(recipient)?; + token.approvals = vec![]; + self.tokens.save(deps.storage, token_id, &token)?; + Ok(token) + } + + #[allow(clippy::too_many_arguments)] + pub fn _update_approvals( + &self, + deps: DepsMut, + env: &Env, + info: &MessageInfo, + spender: &str, + token_id: &str, + // if add == false, remove. if add == true, remove then set with this expiration + add: bool, + expires: Option, + ) -> Result, ContractError> { + let mut token = self.tokens.load(deps.storage, token_id)?; + // ensure we have permissions + self.check_can_approve(deps.as_ref(), env, info, &token)?; + + // update the approval list (remove any for the same spender before adding) + let spender_addr = deps.api.addr_validate(spender)?; + token.approvals = token + .approvals + .into_iter() + .filter(|apr| apr.spender != spender_addr) + .collect(); + + // only difference between approve and revoke + if add { + // reject expired data as invalid + let expires = expires.unwrap_or_default(); + if expires.is_expired(&env.block) { + return Err(ContractError::Expired {}); + } + let approval = Approval { + spender: spender_addr, + expires, + }; + token.approvals.push(approval); + } + + self.tokens.save(deps.storage, token_id, &token)?; + + Ok(token) + } + + /// returns true iff the sender can execute approve or reject on the contract + pub fn check_can_approve( + &self, + deps: Deps, + env: &Env, + info: &MessageInfo, + token: &TokenInfo, + ) -> Result<(), ContractError> { + // owner can approve + if token.owner == info.sender { + return Ok(()); + } + // operator can approve + let op = self + .operators + .may_load(deps.storage, (&token.owner, &info.sender))?; + match op { + Some(ex) => { + if ex.is_expired(&env.block) { + Err(ContractError::Unauthorized {}) + } else { + Ok(()) + } + } + None => Err(ContractError::Unauthorized {}), + } + } + + /// returns true iff the sender can transfer ownership of the token + pub fn check_can_send( + &self, + deps: Deps, + env: &Env, + info: &MessageInfo, + token: &TokenInfo, + ) -> Result<(), ContractError> { + // owner can send + if token.owner == info.sender { + return Ok(()); + } + + // any non-expired token approval can send + if token + .approvals + .iter() + .any(|apr| apr.spender == info.sender && !apr.is_expired(&env.block)) + { + return Ok(()); + } + + // operator can send + let op = self + .operators + .may_load(deps.storage, (&token.owner, &info.sender))?; + match op { + Some(ex) => { + if ex.is_expired(&env.block) { + Err(ContractError::Unauthorized {}) + } else { + Ok(()) + } + } + None => Err(ContractError::Unauthorized {}), + } + } +} diff --git a/wormhole/cw721-base/src/helpers.rs b/wormhole/cw721-base/src/helpers.rs new file mode 100644 index 00000000..437132f5 --- /dev/null +++ b/wormhole/cw721-base/src/helpers.rs @@ -0,0 +1,179 @@ +use crate::{ExecuteMsg, QueryMsg}; +use cosmwasm_std::{to_binary, Addr, CosmosMsg, QuerierWrapper, StdResult, WasmMsg, WasmQuery}; +use cw721::{ + AllNftInfoResponse, Approval, ApprovalResponse, ApprovalsResponse, ContractInfoResponse, + NftInfoResponse, NumTokensResponse, OperatorsResponse, OwnerOfResponse, TokensResponse, +}; +use serde::de::DeserializeOwned; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] +pub struct Cw721Contract(pub Addr); + +#[allow(dead_code)] +impl Cw721Contract { + pub fn addr(&self) -> Addr { + self.0.clone() + } + + pub fn call(&self, msg: ExecuteMsg) -> StdResult { + let msg = to_binary(&msg)?; + Ok(WasmMsg::Execute { + contract_addr: self.addr().into(), + msg, + funds: vec![], + } + .into()) + } + + pub fn query( + &self, + querier: &QuerierWrapper, + req: QueryMsg, + ) -> StdResult { + let query = WasmQuery::Smart { + contract_addr: self.addr().into(), + msg: to_binary(&req)?, + } + .into(); + querier.query(&query) + } + + /*** queries ***/ + + pub fn owner_of>( + &self, + querier: &QuerierWrapper, + token_id: T, + include_expired: bool, + ) -> StdResult { + let req = QueryMsg::OwnerOf { + token_id: token_id.into(), + include_expired: Some(include_expired), + }; + self.query(querier, req) + } + + pub fn approval>( + &self, + querier: &QuerierWrapper, + token_id: T, + spender: T, + include_expired: Option, + ) -> StdResult { + let req = QueryMsg::Approval { + token_id: token_id.into(), + spender: spender.into(), + include_expired, + }; + let res: ApprovalResponse = self.query(querier, req)?; + Ok(res) + } + + pub fn approvals>( + &self, + querier: &QuerierWrapper, + token_id: T, + include_expired: Option, + ) -> StdResult { + let req = QueryMsg::Approvals { + token_id: token_id.into(), + include_expired, + }; + let res: ApprovalsResponse = self.query(querier, req)?; + Ok(res) + } + + pub fn all_operators>( + &self, + querier: &QuerierWrapper, + owner: T, + include_expired: bool, + start_after: Option, + limit: Option, + ) -> StdResult> { + let req = QueryMsg::AllOperators { + owner: owner.into(), + include_expired: Some(include_expired), + start_after, + limit, + }; + let res: OperatorsResponse = self.query(querier, req)?; + Ok(res.operators) + } + + pub fn num_tokens(&self, querier: &QuerierWrapper) -> StdResult { + let req = QueryMsg::NumTokens {}; + let res: NumTokensResponse = self.query(querier, req)?; + Ok(res.count) + } + + /// With metadata extension + pub fn contract_info(&self, querier: &QuerierWrapper) -> StdResult { + let req = QueryMsg::ContractInfo {}; + self.query(querier, req) + } + + /// With metadata extension + pub fn nft_info, U: DeserializeOwned>( + &self, + querier: &QuerierWrapper, + token_id: T, + ) -> StdResult> { + let req = QueryMsg::NftInfo { + token_id: token_id.into(), + }; + self.query(querier, req) + } + + /// With metadata extension + pub fn all_nft_info, U: DeserializeOwned>( + &self, + querier: &QuerierWrapper, + token_id: T, + include_expired: bool, + ) -> StdResult> { + let req = QueryMsg::AllNftInfo { + token_id: token_id.into(), + include_expired: Some(include_expired), + }; + self.query(querier, req) + } + + /// With enumerable extension + pub fn tokens>( + &self, + querier: &QuerierWrapper, + owner: T, + start_after: Option, + limit: Option, + ) -> StdResult { + let req = QueryMsg::Tokens { + owner: owner.into(), + start_after, + limit, + }; + self.query(querier, req) + } + + /// With enumerable extension + pub fn all_tokens( + &self, + querier: &QuerierWrapper, + start_after: Option, + limit: Option, + ) -> StdResult { + let req = QueryMsg::AllTokens { start_after, limit }; + self.query(querier, req) + } + + /// returns true if the contract supports the metadata extension + pub fn has_metadata(&self, querier: &QuerierWrapper) -> bool { + self.contract_info(querier).is_ok() + } + + /// returns true if the contract supports the enumerable extension + pub fn has_enumerable(&self, querier: &QuerierWrapper) -> bool { + self.tokens(querier, self.addr(), None, Some(1)).is_ok() + } +} diff --git a/wormhole/cw721-base/src/lib.rs b/wormhole/cw721-base/src/lib.rs new file mode 100644 index 00000000..bc02d289 --- /dev/null +++ b/wormhole/cw721-base/src/lib.rs @@ -0,0 +1,50 @@ +mod contract_tests; +mod error; +mod execute; +pub mod helpers; +pub mod msg; +mod query; +pub mod state; + +pub use crate::error::ContractError; +pub use crate::msg::{ExecuteMsg, InstantiateMsg, MintMsg, MinterResponse, QueryMsg}; +pub use crate::state::Cw721Contract; +use cosmwasm_std::Empty; + +pub type Extension = Option; + +#[cfg(not(feature = "library"))] +pub mod entry { + use super::*; + + use cosmwasm_std::{entry_point, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult}; + + // This makes a conscious choice on the various generics used by the contract + #[entry_point] + pub fn instantiate( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: InstantiateMsg, + ) -> StdResult { + let tract = Cw721Contract::::default(); + tract.instantiate(deps, env, info, msg) + } + + #[entry_point] + pub fn execute( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: ExecuteMsg, + ) -> Result { + let tract = Cw721Contract::::default(); + tract.execute(deps, env, info, msg) + } + + #[entry_point] + pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { + let tract = Cw721Contract::::default(); + tract.query(deps, env, msg) + } +} diff --git a/wormhole/cw721-base/src/msg.rs b/wormhole/cw721-base/src/msg.rs new file mode 100644 index 00000000..6d2994e7 --- /dev/null +++ b/wormhole/cw721-base/src/msg.rs @@ -0,0 +1,154 @@ +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use cosmwasm_std::Binary; +use cw721::Expiration; + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +pub struct InstantiateMsg { + /// Name of the NFT contract + pub name: String, + /// Symbol of the NFT contract + pub symbol: String, + + /// The minter is the only one who can create new NFTs. + /// This is designed for a base NFT that is controlled by an external program + /// or contract. You will likely replace this with custom logic in custom NFTs + pub minter: String, +} + +/// This is like Cw721ExecuteMsg but we add a Mint command for an owner +/// to make this stand-alone. You will likely want to remove mint and +/// use other control logic in any contract that inherits this. +#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] +#[serde(rename_all = "snake_case")] +pub enum ExecuteMsg { + /// Transfer is a base message to move a token to another account without triggering actions + TransferNft { recipient: String, token_id: String }, + /// Send is a base message to transfer a token to a contract and trigger an action + /// on the receiving contract. + SendNft { + contract: String, + token_id: String, + msg: Binary, + }, + /// Allows operator to transfer / send the token from the owner's account. + /// If expiration is set, then this allowance has a time/height limit + Approve { + spender: String, + token_id: String, + expires: Option, + }, + /// Remove previously granted Approval + Revoke { spender: String, token_id: String }, + /// Allows operator to transfer / send any token from the owner's account. + /// If expiration is set, then this allowance has a time/height limit + ApproveAll { + operator: String, + expires: Option, + }, + /// Remove previously granted ApproveAll permission + RevokeAll { operator: String }, + + /// Mint a new NFT, can only be called by the contract minter + Mint(MintMsg), + + /// Burn an NFT the sender has access to + Burn { token_id: String }, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +pub struct MintMsg { + /// Unique ID of the NFT + pub token_id: String, + /// The owner of the newly minter NFT + pub owner: String, + /// Universal resource identifier for this NFT + /// Should point to a JSON file that conforms to the ERC721 + /// Metadata JSON Schema + pub token_uri: Option, + /// Any custom extension used by this contract + pub extension: T, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum QueryMsg { + /// Return the owner of the given token, error if token does not exist + /// Return type: OwnerOfResponse + OwnerOf { + token_id: String, + /// unset or false will filter out expired approvals, you must set to true to see them + include_expired: Option, + }, + + /// Return operator that can access all of the owner's tokens. + /// Return type: `ApprovalResponse` + Approval { + token_id: String, + spender: String, + include_expired: Option, + }, + + /// Return approvals that a token has + /// Return type: `ApprovalsResponse` + Approvals { + token_id: String, + include_expired: Option, + }, + + /// List all operators that can access all of the owner's tokens + /// Return type: `OperatorsResponse` + AllOperators { + owner: String, + /// unset or false will filter out expired items, you must set to true to see them + include_expired: Option, + start_after: Option, + limit: Option, + }, + /// Total number of tokens issued + NumTokens {}, + + /// With MetaData Extension. + /// Returns top-level metadata about the contract: `ContractInfoResponse` + ContractInfo {}, + /// With MetaData Extension. + /// Returns metadata about one particular token, based on *ERC721 Metadata JSON Schema* + /// but directly from the contract: `NftInfoResponse` + NftInfo { + token_id: String, + }, + /// With MetaData Extension. + /// Returns the result of both `NftInfo` and `OwnerOf` as one query as an optimization + /// for clients: `AllNftInfo` + AllNftInfo { + token_id: String, + /// unset or false will filter out expired approvals, you must set to true to see them + include_expired: Option, + }, + + /// With Enumerable extension. + /// Returns all tokens owned by the given address, [] if unset. + /// Return type: TokensResponse. + Tokens { + owner: String, + start_after: Option, + limit: Option, + }, + /// With Enumerable extension. + /// Requires pagination. Lists all token_ids controlled by the contract. + /// Return type: TokensResponse. + AllTokens { + start_after: Option, + limit: Option, + }, + + // Return the minter + Minter {}, +} + +/// Shows who can mint these tokens +#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] +pub struct MinterResponse { + pub minter: String, +} diff --git a/wormhole/cw721-base/src/query.rs b/wormhole/cw721-base/src/query.rs new file mode 100644 index 00000000..1a597168 --- /dev/null +++ b/wormhole/cw721-base/src/query.rs @@ -0,0 +1,293 @@ +use serde::de::DeserializeOwned; +use serde::Serialize; + +use cosmwasm_std::{to_binary, Binary, BlockInfo, Deps, Env, Order, StdError, StdResult}; + +use cw0::maybe_addr; +use cw721::{ + AllNftInfoResponse, ApprovalResponse, ApprovalsResponse, ContractInfoResponse, CustomMsg, + Cw721Query, NftInfoResponse, NumTokensResponse, OperatorsResponse, OwnerOfResponse, + TokensResponse, +}; +use cw_storage_plus::Bound; + +use crate::msg::{MinterResponse, QueryMsg}; +use crate::state::{Approval, Cw721Contract, TokenInfo}; + +const DEFAULT_LIMIT: u32 = 10; +const MAX_LIMIT: u32 = 30; + +impl<'a, T, C> Cw721Query for Cw721Contract<'a, T, C> +where + T: Serialize + DeserializeOwned + Clone, + C: CustomMsg, +{ + fn contract_info(&self, deps: Deps) -> StdResult { + self.contract_info.load(deps.storage) + } + + fn num_tokens(&self, deps: Deps) -> StdResult { + let count = self.token_count(deps.storage)?; + Ok(NumTokensResponse { count }) + } + + fn nft_info(&self, deps: Deps, token_id: String) -> StdResult> { + let info = self.tokens.load(deps.storage, &token_id)?; + Ok(NftInfoResponse { + token_uri: info.token_uri, + extension: info.extension, + }) + } + + fn owner_of( + &self, + deps: Deps, + env: Env, + token_id: String, + include_expired: bool, + ) -> StdResult { + let info = self.tokens.load(deps.storage, &token_id)?; + Ok(OwnerOfResponse { + owner: info.owner.to_string(), + approvals: humanize_approvals(&env.block, &info, include_expired), + }) + } + + /// operators returns all operators owner given access to + fn operators( + &self, + deps: Deps, + env: Env, + owner: String, + include_expired: bool, + start_after: Option, + limit: Option, + ) -> StdResult { + let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize; + let start_addr = maybe_addr(deps.api, start_after)?; + let start = start_addr.as_ref().map(Bound::exclusive); + + let owner_addr = deps.api.addr_validate(&owner)?; + let operators = self + .operators + .prefix(&owner_addr) + .range(deps.storage, start, None, Order::Ascending) + .filter(|r| { + include_expired || r.is_err() || !r.as_ref().unwrap().1.is_expired(&env.block) + }) + .take(limit) + .map(|item| { + item.map(|(k, expires)| cw721::Approval { + spender: k.into_string(), + expires, + }) + }) + .collect::>()?; + Ok(OperatorsResponse { operators }) + } + + fn approval( + &self, + deps: Deps, + env: Env, + token_id: String, + spender: String, + include_expired: bool, + ) -> StdResult { + let token = self.tokens.load(deps.storage, &token_id)?; + let filtered: Vec<_> = token + .approvals + .into_iter() + .filter(|t| t.spender == spender) + .filter(|t| include_expired || !t.is_expired(&env.block)) + .map(|a| cw721::Approval { + spender: a.spender.into_string(), + expires: a.expires, + }) + .collect(); + + if filtered.is_empty() { + return Err(StdError::not_found("Approval not found")); + } + // we expect only one item + let approval = filtered[0].clone(); + + Ok(ApprovalResponse { approval }) + } + + /// approvals returns all approvals owner given access to + fn approvals( + &self, + deps: Deps, + env: Env, + token_id: String, + include_expired: bool, + ) -> StdResult { + let token = self.tokens.load(deps.storage, &token_id)?; + let approvals: Vec<_> = token + .approvals + .into_iter() + .filter(|t| include_expired || !t.is_expired(&env.block)) + .map(|a| cw721::Approval { + spender: a.spender.into_string(), + expires: a.expires, + }) + .collect(); + + Ok(ApprovalsResponse { approvals }) + } + + fn tokens( + &self, + deps: Deps, + owner: String, + start_after: Option, + limit: Option, + ) -> StdResult { + let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize; + let start = start_after.as_deref().map(Bound::exclusive); + + let owner_addr = deps.api.addr_validate(&owner)?; + let tokens: Vec<_> = self + .tokens + .idx + .owner + .prefix((owner_addr, vec![])) + .keys(deps.storage, start, None, Order::Ascending) + .take(limit) + .collect::>()?; + Ok(TokensResponse { tokens }) + } + + fn all_tokens( + &self, + deps: Deps, + start_after: Option, + limit: Option, + ) -> StdResult { + let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize; + let start = start_after.as_deref().map(Bound::exclusive); + + let tokens: Vec = self + .tokens + .range(deps.storage, start, None, Order::Ascending) + .take(limit) + .map(|item| item.map(|(k, _)| k)) + .collect::>()?; + Ok(TokensResponse { tokens }) + } + + fn all_nft_info( + &self, + deps: Deps, + env: Env, + token_id: String, + include_expired: bool, + ) -> StdResult> { + let info = self.tokens.load(deps.storage, &token_id)?; + Ok(AllNftInfoResponse { + access: OwnerOfResponse { + owner: info.owner.to_string(), + approvals: humanize_approvals(&env.block, &info, include_expired), + }, + info: NftInfoResponse { + token_uri: info.token_uri, + extension: info.extension, + }, + }) + } +} + +impl<'a, T, C> Cw721Contract<'a, T, C> +where + T: Serialize + DeserializeOwned + Clone, + C: CustomMsg, +{ + pub fn minter(&self, deps: Deps) -> StdResult { + let minter_addr = self.minter.load(deps.storage)?; + Ok(MinterResponse { + minter: minter_addr.to_string(), + }) + } + + pub fn query(&self, deps: Deps, env: Env, msg: QueryMsg) -> StdResult { + match msg { + QueryMsg::Minter {} => to_binary(&self.minter(deps)?), + QueryMsg::ContractInfo {} => to_binary(&self.contract_info(deps)?), + QueryMsg::NftInfo { token_id } => to_binary(&self.nft_info(deps, token_id)?), + QueryMsg::OwnerOf { + token_id, + include_expired, + } => { + to_binary(&self.owner_of(deps, env, token_id, include_expired.unwrap_or(false))?) + } + QueryMsg::AllNftInfo { + token_id, + include_expired, + } => to_binary(&self.all_nft_info( + deps, + env, + token_id, + include_expired.unwrap_or(false), + )?), + QueryMsg::AllOperators { + owner, + include_expired, + start_after, + limit, + } => to_binary(&self.operators( + deps, + env, + owner, + include_expired.unwrap_or(false), + start_after, + limit, + )?), + QueryMsg::NumTokens {} => to_binary(&self.num_tokens(deps)?), + QueryMsg::Tokens { + owner, + start_after, + limit, + } => to_binary(&self.tokens(deps, owner, start_after, limit)?), + QueryMsg::AllTokens { start_after, limit } => { + to_binary(&self.all_tokens(deps, start_after, limit)?) + } + QueryMsg::Approval { + token_id, + spender, + include_expired, + } => to_binary(&self.approval( + deps, + env, + token_id, + spender, + include_expired.unwrap_or(false), + )?), + QueryMsg::Approvals { + token_id, + include_expired, + } => { + to_binary(&self.approvals(deps, env, token_id, include_expired.unwrap_or(false))?) + } + } + } +} + +fn humanize_approvals( + block: &BlockInfo, + info: &TokenInfo, + include_expired: bool, +) -> Vec { + info.approvals + .iter() + .filter(|apr| include_expired || !apr.is_expired(block)) + .map(humanize_approval) + .collect() +} + +fn humanize_approval(approval: &Approval) -> cw721::Approval { + cw721::Approval { + spender: approval.spender.to_string(), + expires: approval.expires, + } +} diff --git a/wormhole/cw721-base/src/state.rs b/wormhole/cw721-base/src/state.rs new file mode 100644 index 00000000..07fedf0b --- /dev/null +++ b/wormhole/cw721-base/src/state.rs @@ -0,0 +1,141 @@ +use schemars::JsonSchema; +use serde::de::DeserializeOwned; +use serde::{Deserialize, Serialize}; +use std::marker::PhantomData; + +use cosmwasm_std::{Addr, BlockInfo, StdResult, Storage}; + +use cw721::{ContractInfoResponse, CustomMsg, Cw721, Expiration}; +use cw_storage_plus::{Index, IndexList, IndexedMap, Item, Map, MultiIndex}; + +pub struct Cw721Contract<'a, T, C> +where + T: Serialize + DeserializeOwned + Clone, +{ + pub contract_info: Item<'a, ContractInfoResponse>, + pub minter: Item<'a, Addr>, + pub token_count: Item<'a, u64>, + /// Stored as (granter, operator) giving operator full control over granter's account + pub operators: Map<'a, (&'a Addr, &'a Addr), Expiration>, + pub tokens: IndexedMap<'a, &'a str, TokenInfo, TokenIndexes<'a, T>>, + + pub(crate) _custom_response: PhantomData, +} + +// This is a signal, the implementations are in other files +impl<'a, T, C> Cw721 for Cw721Contract<'a, T, C> +where + T: Serialize + DeserializeOwned + Clone, + C: CustomMsg, +{ +} + +impl Default for Cw721Contract<'static, T, C> +where + T: Serialize + DeserializeOwned + Clone, +{ + fn default() -> Self { + Self::new( + "nft_info", + "minter", + "num_tokens", + "operators", + "tokens", + "tokens__owner", + ) + } +} + +impl<'a, T, C> Cw721Contract<'a, T, C> +where + T: Serialize + DeserializeOwned + Clone, +{ + fn new( + contract_key: &'a str, + minter_key: &'a str, + token_count_key: &'a str, + operator_key: &'a str, + tokens_key: &'a str, + tokens_owner_key: &'a str, + ) -> Self { + let indexes = TokenIndexes { + owner: MultiIndex::new(token_owner_idx, tokens_key, tokens_owner_key), + }; + Self { + contract_info: Item::new(contract_key), + minter: Item::new(minter_key), + token_count: Item::new(token_count_key), + operators: Map::new(operator_key), + tokens: IndexedMap::new(tokens_key, indexes), + _custom_response: PhantomData, + } + } + + pub fn token_count(&self, storage: &dyn Storage) -> StdResult { + Ok(self.token_count.may_load(storage)?.unwrap_or_default()) + } + + pub fn increment_tokens(&self, storage: &mut dyn Storage) -> StdResult { + let val = self.token_count(storage)? + 1; + self.token_count.save(storage, &val)?; + Ok(val) + } + + pub fn decrement_tokens(&self, storage: &mut dyn Storage) -> StdResult { + let val = self.token_count(storage)? - 1; + self.token_count.save(storage, &val)?; + Ok(val) + } +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +pub struct TokenInfo { + /// The owner of the newly minted NFT + pub owner: Addr, + /// Approvals are stored here, as we clear them all upon transfer and cannot accumulate much + pub approvals: Vec, + + /// Universal resource identifier for this NFT + /// Should point to a JSON file that conforms to the ERC721 + /// Metadata JSON Schema + pub token_uri: Option, + + /// You can add any custom metadata here when you extend cw721-base + pub extension: T, +} + +#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] +pub struct Approval { + /// Account that can transfer/send the token + pub spender: Addr, + /// When the Approval expires (maybe Expiration::never) + pub expires: Expiration, +} + +impl Approval { + pub fn is_expired(&self, block: &BlockInfo) -> bool { + self.expires.is_expired(block) + } +} + +pub struct TokenIndexes<'a, T> +where + T: Serialize + DeserializeOwned + Clone, +{ + // pk goes to second tuple element + pub owner: MultiIndex<'a, (Addr, Vec), TokenInfo, &'a str>, +} + +impl<'a, T> IndexList> for TokenIndexes<'a, T> +where + T: Serialize + DeserializeOwned + Clone, +{ + fn get_indexes(&'_ self) -> Box>> + '_> { + let v: Vec<&dyn Index>> = vec![&self.owner]; + Box::new(v.into_iter()) + } +} + +pub fn token_owner_idx(d: &TokenInfo) -> (Addr, Vec) { + (d.owner.clone(), vec![]) +} diff --git a/wormhole/cw721-wrapped/.cargo/config b/wormhole/cw721-wrapped/.cargo/config new file mode 100644 index 00000000..7d1a066c --- /dev/null +++ b/wormhole/cw721-wrapped/.cargo/config @@ -0,0 +1,5 @@ +[alias] +wasm = "build --release --target wasm32-unknown-unknown" +wasm-debug = "build --target wasm32-unknown-unknown" +unit-test = "test --lib" +schema = "run --example schema" diff --git a/wormhole/cw721-wrapped/Cargo.toml b/wormhole/cw721-wrapped/Cargo.toml new file mode 100644 index 00000000..ffbbf22b --- /dev/null +++ b/wormhole/cw721-wrapped/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "cw721-wrapped" +version = "0.10.1" +edition = "2018" +description = "Wrapped CW721 token contract" + +exclude = [ + "artifacts/*", +] + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +# for more explicit tests, cargo test --features=backtraces +backtraces = ["cosmwasm-std/backtraces"] +# use library feature to disable all instantiate/execute/query exports +library = [] + +[dependencies] +cosmwasm-std = "1.0.0" +cosmwasm-storage = { version = "1.0.0" } +cw2 = "0.13.4" +cw721 = { path = "../../packages/cw721", version = "0.10.1" } +cw721-base = { path = "../cw721-base", version = "0.10.0", features = ["library"] } +schemars = "0.8.10" +serde = { version = "1.0.137", default-features = false, features = ["derive"] } + +[dev-dependencies] +cosmwasm-schema = "1.0.0" diff --git a/wormhole/cw721-wrapped/examples/cw721_wrapped_schema.rs b/wormhole/cw721-wrapped/examples/cw721_wrapped_schema.rs new file mode 100644 index 00000000..9832342b --- /dev/null +++ b/wormhole/cw721-wrapped/examples/cw721_wrapped_schema.rs @@ -0,0 +1,39 @@ +use std::env::current_dir; +use std::fs::create_dir_all; + +use cosmwasm_schema::{export_schema, export_schema_with_title, remove_schemas, schema_for}; + +use cw721::{ + AllNftInfoResponse, ApprovalResponse, ApprovalsResponse, ContractInfoResponse, NftInfoResponse, + NumTokensResponse, OperatorsResponse, OwnerOfResponse, TokensResponse, +}; +use cw721_base::{ExecuteMsg, Extension, InstantiateMsg, MinterResponse, QueryMsg}; + +fn main() { + let mut out_dir = current_dir().unwrap(); + out_dir.push("schema"); + create_dir_all(&out_dir).unwrap(); + remove_schemas(&out_dir).unwrap(); + + export_schema(&schema_for!(InstantiateMsg), &out_dir); + export_schema_with_title(&schema_for!(ExecuteMsg<()>), &out_dir, "ExecuteMsg"); + export_schema(&schema_for!(QueryMsg), &out_dir); + export_schema_with_title( + &schema_for!(AllNftInfoResponse), + &out_dir, + "AllNftInfoResponse", + ); + export_schema(&schema_for!(ApprovalResponse), &out_dir); + export_schema(&schema_for!(ApprovalsResponse), &out_dir); + export_schema(&schema_for!(OperatorsResponse), &out_dir); + export_schema(&schema_for!(ContractInfoResponse), &out_dir); + export_schema(&schema_for!(MinterResponse), &out_dir); + export_schema_with_title( + &schema_for!(NftInfoResponse), + &out_dir, + "NftInfoResponse", + ); + export_schema(&schema_for!(NumTokensResponse), &out_dir); + export_schema(&schema_for!(OwnerOfResponse), &out_dir); + export_schema(&schema_for!(TokensResponse), &out_dir); +} diff --git a/wormhole/cw721-wrapped/schema/all_nft_info_response.json b/wormhole/cw721-wrapped/schema/all_nft_info_response.json new file mode 100644 index 00000000..bfc334bf --- /dev/null +++ b/wormhole/cw721-wrapped/schema/all_nft_info_response.json @@ -0,0 +1,155 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "AllNftInfoResponse", + "type": "object", + "required": [ + "access", + "info" + ], + "properties": { + "access": { + "description": "Who can transfer the token", + "allOf": [ + { + "$ref": "#/definitions/OwnerOfResponse" + } + ] + }, + "info": { + "description": "Data on the token itself,", + "allOf": [ + { + "$ref": "#/definitions/NftInfoResponse_for_Nullable_Empty" + } + ] + } + }, + "definitions": { + "Approval": { + "type": "object", + "required": [ + "expires", + "spender" + ], + "properties": { + "expires": { + "description": "When the Approval expires (maybe Expiration::never)", + "allOf": [ + { + "$ref": "#/definitions/Expiration" + } + ] + }, + "spender": { + "description": "Account that can transfer/send the token", + "type": "string" + } + } + }, + "Empty": { + "description": "An empty struct that serves as a placeholder in different places, such as contracts that don't set a custom message.\n\nIt is designed to be expressable in correct JSON and JSON Schema but contains no meaningful data. Previously we used enums without cases, but those cannot represented as valid JSON Schema (https://github.com/CosmWasm/cosmwasm/issues/451)", + "type": "object" + }, + "Expiration": { + "description": "Expiration represents a point in time when some event happens. It can compare with a BlockInfo and will return is_expired() == true once the condition is hit (and for every block in the future)", + "oneOf": [ + { + "description": "AtHeight will expire when `env.block.height` >= height", + "type": "object", + "required": [ + "at_height" + ], + "properties": { + "at_height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + { + "description": "AtTime will expire when `env.block.time` >= time", + "type": "object", + "required": [ + "at_time" + ], + "properties": { + "at_time": { + "$ref": "#/definitions/Timestamp" + } + }, + "additionalProperties": false + }, + { + "description": "Never will never expire. Used to express the empty variant", + "type": "object", + "required": [ + "never" + ], + "properties": { + "never": { + "type": "object" + } + }, + "additionalProperties": false + } + ] + }, + "NftInfoResponse_for_Nullable_Empty": { + "type": "object", + "properties": { + "extension": { + "description": "You can add any custom metadata here when you extend cw721-base", + "anyOf": [ + { + "$ref": "#/definitions/Empty" + }, + { + "type": "null" + } + ] + }, + "token_uri": { + "description": "Universal resource identifier for this NFT Should point to a JSON file that conforms to the ERC721 Metadata JSON Schema", + "type": [ + "string", + "null" + ] + } + } + }, + "OwnerOfResponse": { + "type": "object", + "required": [ + "approvals", + "owner" + ], + "properties": { + "approvals": { + "description": "If set this address is approved to transfer/send the token as well", + "type": "array", + "items": { + "$ref": "#/definitions/Approval" + } + }, + "owner": { + "description": "Owner of the token", + "type": "string" + } + } + }, + "Timestamp": { + "description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```", + "allOf": [ + { + "$ref": "#/definitions/Uint64" + } + ] + }, + "Uint64": { + "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```", + "type": "string" + } + } +} diff --git a/wormhole/cw721-wrapped/schema/approval_response.json b/wormhole/cw721-wrapped/schema/approval_response.json new file mode 100644 index 00000000..4f45b42e --- /dev/null +++ b/wormhole/cw721-wrapped/schema/approval_response.json @@ -0,0 +1,94 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ApprovalResponse", + "type": "object", + "required": [ + "approval" + ], + "properties": { + "approval": { + "$ref": "#/definitions/Approval" + } + }, + "definitions": { + "Approval": { + "type": "object", + "required": [ + "expires", + "spender" + ], + "properties": { + "expires": { + "description": "When the Approval expires (maybe Expiration::never)", + "allOf": [ + { + "$ref": "#/definitions/Expiration" + } + ] + }, + "spender": { + "description": "Account that can transfer/send the token", + "type": "string" + } + } + }, + "Expiration": { + "description": "Expiration represents a point in time when some event happens. It can compare with a BlockInfo and will return is_expired() == true once the condition is hit (and for every block in the future)", + "oneOf": [ + { + "description": "AtHeight will expire when `env.block.height` >= height", + "type": "object", + "required": [ + "at_height" + ], + "properties": { + "at_height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + { + "description": "AtTime will expire when `env.block.time` >= time", + "type": "object", + "required": [ + "at_time" + ], + "properties": { + "at_time": { + "$ref": "#/definitions/Timestamp" + } + }, + "additionalProperties": false + }, + { + "description": "Never will never expire. Used to express the empty variant", + "type": "object", + "required": [ + "never" + ], + "properties": { + "never": { + "type": "object" + } + }, + "additionalProperties": false + } + ] + }, + "Timestamp": { + "description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```", + "allOf": [ + { + "$ref": "#/definitions/Uint64" + } + ] + }, + "Uint64": { + "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```", + "type": "string" + } + } +} diff --git a/wormhole/cw721-wrapped/schema/approvals_response.json b/wormhole/cw721-wrapped/schema/approvals_response.json new file mode 100644 index 00000000..8d8e39ea --- /dev/null +++ b/wormhole/cw721-wrapped/schema/approvals_response.json @@ -0,0 +1,97 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ApprovalsResponse", + "type": "object", + "required": [ + "approvals" + ], + "properties": { + "approvals": { + "type": "array", + "items": { + "$ref": "#/definitions/Approval" + } + } + }, + "definitions": { + "Approval": { + "type": "object", + "required": [ + "expires", + "spender" + ], + "properties": { + "expires": { + "description": "When the Approval expires (maybe Expiration::never)", + "allOf": [ + { + "$ref": "#/definitions/Expiration" + } + ] + }, + "spender": { + "description": "Account that can transfer/send the token", + "type": "string" + } + } + }, + "Expiration": { + "description": "Expiration represents a point in time when some event happens. It can compare with a BlockInfo and will return is_expired() == true once the condition is hit (and for every block in the future)", + "oneOf": [ + { + "description": "AtHeight will expire when `env.block.height` >= height", + "type": "object", + "required": [ + "at_height" + ], + "properties": { + "at_height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + { + "description": "AtTime will expire when `env.block.time` >= time", + "type": "object", + "required": [ + "at_time" + ], + "properties": { + "at_time": { + "$ref": "#/definitions/Timestamp" + } + }, + "additionalProperties": false + }, + { + "description": "Never will never expire. Used to express the empty variant", + "type": "object", + "required": [ + "never" + ], + "properties": { + "never": { + "type": "object" + } + }, + "additionalProperties": false + } + ] + }, + "Timestamp": { + "description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```", + "allOf": [ + { + "$ref": "#/definitions/Uint64" + } + ] + }, + "Uint64": { + "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```", + "type": "string" + } + } +} diff --git a/wormhole/cw721-wrapped/schema/contract_info_response.json b/wormhole/cw721-wrapped/schema/contract_info_response.json new file mode 100644 index 00000000..a1671258 --- /dev/null +++ b/wormhole/cw721-wrapped/schema/contract_info_response.json @@ -0,0 +1,17 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ContractInfoResponse", + "type": "object", + "required": [ + "name", + "symbol" + ], + "properties": { + "name": { + "type": "string" + }, + "symbol": { + "type": "string" + } + } +} diff --git a/wormhole/cw721-wrapped/schema/execute_msg.json b/wormhole/cw721-wrapped/schema/execute_msg.json new file mode 100644 index 00000000..e8cc795f --- /dev/null +++ b/wormhole/cw721-wrapped/schema/execute_msg.json @@ -0,0 +1,300 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ExecuteMsg", + "description": "This is like Cw721ExecuteMsg but we add a Mint command for an owner to make this stand-alone. You will likely want to remove mint and use other control logic in any contract that inherits this.", + "oneOf": [ + { + "description": "Transfer is a base message to move a token to another account without triggering actions", + "type": "object", + "required": [ + "transfer_nft" + ], + "properties": { + "transfer_nft": { + "type": "object", + "required": [ + "recipient", + "token_id" + ], + "properties": { + "recipient": { + "type": "string" + }, + "token_id": { + "type": "string" + } + } + } + }, + "additionalProperties": false + }, + { + "description": "Send is a base message to transfer a token to a contract and trigger an action on the receiving contract.", + "type": "object", + "required": [ + "send_nft" + ], + "properties": { + "send_nft": { + "type": "object", + "required": [ + "contract", + "msg", + "token_id" + ], + "properties": { + "contract": { + "type": "string" + }, + "msg": { + "$ref": "#/definitions/Binary" + }, + "token_id": { + "type": "string" + } + } + } + }, + "additionalProperties": false + }, + { + "description": "Allows operator to transfer / send the token from the owner's account. If expiration is set, then this allowance has a time/height limit", + "type": "object", + "required": [ + "approve" + ], + "properties": { + "approve": { + "type": "object", + "required": [ + "spender", + "token_id" + ], + "properties": { + "expires": { + "anyOf": [ + { + "$ref": "#/definitions/Expiration" + }, + { + "type": "null" + } + ] + }, + "spender": { + "type": "string" + }, + "token_id": { + "type": "string" + } + } + } + }, + "additionalProperties": false + }, + { + "description": "Remove previously granted Approval", + "type": "object", + "required": [ + "revoke" + ], + "properties": { + "revoke": { + "type": "object", + "required": [ + "spender", + "token_id" + ], + "properties": { + "spender": { + "type": "string" + }, + "token_id": { + "type": "string" + } + } + } + }, + "additionalProperties": false + }, + { + "description": "Allows operator to transfer / send any token from the owner's account. If expiration is set, then this allowance has a time/height limit", + "type": "object", + "required": [ + "approve_all" + ], + "properties": { + "approve_all": { + "type": "object", + "required": [ + "operator" + ], + "properties": { + "expires": { + "anyOf": [ + { + "$ref": "#/definitions/Expiration" + }, + { + "type": "null" + } + ] + }, + "operator": { + "type": "string" + } + } + } + }, + "additionalProperties": false + }, + { + "description": "Remove previously granted ApproveAll permission", + "type": "object", + "required": [ + "revoke_all" + ], + "properties": { + "revoke_all": { + "type": "object", + "required": [ + "operator" + ], + "properties": { + "operator": { + "type": "string" + } + } + } + }, + "additionalProperties": false + }, + { + "description": "Mint a new NFT, can only be called by the contract minter", + "type": "object", + "required": [ + "mint" + ], + "properties": { + "mint": { + "$ref": "#/definitions/MintMsg_for_Null" + } + }, + "additionalProperties": false + }, + { + "description": "Burn an NFT the sender has access to", + "type": "object", + "required": [ + "burn" + ], + "properties": { + "burn": { + "type": "object", + "required": [ + "token_id" + ], + "properties": { + "token_id": { + "type": "string" + } + } + } + }, + "additionalProperties": false + } + ], + "definitions": { + "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", + "type": "string" + }, + "Expiration": { + "description": "Expiration represents a point in time when some event happens. It can compare with a BlockInfo and will return is_expired() == true once the condition is hit (and for every block in the future)", + "oneOf": [ + { + "description": "AtHeight will expire when `env.block.height` >= height", + "type": "object", + "required": [ + "at_height" + ], + "properties": { + "at_height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + { + "description": "AtTime will expire when `env.block.time` >= time", + "type": "object", + "required": [ + "at_time" + ], + "properties": { + "at_time": { + "$ref": "#/definitions/Timestamp" + } + }, + "additionalProperties": false + }, + { + "description": "Never will never expire. Used to express the empty variant", + "type": "object", + "required": [ + "never" + ], + "properties": { + "never": { + "type": "object" + } + }, + "additionalProperties": false + } + ] + }, + "MintMsg_for_Null": { + "type": "object", + "required": [ + "extension", + "owner", + "token_id" + ], + "properties": { + "extension": { + "description": "Any custom extension used by this contract", + "type": "null" + }, + "owner": { + "description": "The owner of the newly minter NFT", + "type": "string" + }, + "token_id": { + "description": "Unique ID of the NFT", + "type": "string" + }, + "token_uri": { + "description": "Universal resource identifier for this NFT Should point to a JSON file that conforms to the ERC721 Metadata JSON Schema", + "type": [ + "string", + "null" + ] + } + } + }, + "Timestamp": { + "description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```", + "allOf": [ + { + "$ref": "#/definitions/Uint64" + } + ] + }, + "Uint64": { + "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```", + "type": "string" + } + } +} diff --git a/wormhole/cw721-wrapped/schema/instantiate_msg.json b/wormhole/cw721-wrapped/schema/instantiate_msg.json new file mode 100644 index 00000000..b024c82c --- /dev/null +++ b/wormhole/cw721-wrapped/schema/instantiate_msg.json @@ -0,0 +1,24 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "InstantiateMsg", + "type": "object", + "required": [ + "minter", + "name", + "symbol" + ], + "properties": { + "minter": { + "description": "The minter is the only one who can create new NFTs. This is designed for a base NFT that is controlled by an external program or contract. You will likely replace this with custom logic in custom NFTs", + "type": "string" + }, + "name": { + "description": "Name of the NFT contract", + "type": "string" + }, + "symbol": { + "description": "Symbol of the NFT contract", + "type": "string" + } + } +} diff --git a/wormhole/cw721-wrapped/schema/minter_response.json b/wormhole/cw721-wrapped/schema/minter_response.json new file mode 100644 index 00000000..a20e0d76 --- /dev/null +++ b/wormhole/cw721-wrapped/schema/minter_response.json @@ -0,0 +1,14 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "MinterResponse", + "description": "Shows who can mint these tokens", + "type": "object", + "required": [ + "minter" + ], + "properties": { + "minter": { + "type": "string" + } + } +} diff --git a/wormhole/cw721-wrapped/schema/nft_info_response.json b/wormhole/cw721-wrapped/schema/nft_info_response.json new file mode 100644 index 00000000..e6bf1d40 --- /dev/null +++ b/wormhole/cw721-wrapped/schema/nft_info_response.json @@ -0,0 +1,31 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "NftInfoResponse", + "type": "object", + "properties": { + "extension": { + "description": "You can add any custom metadata here when you extend cw721-base", + "anyOf": [ + { + "$ref": "#/definitions/Empty" + }, + { + "type": "null" + } + ] + }, + "token_uri": { + "description": "Universal resource identifier for this NFT Should point to a JSON file that conforms to the ERC721 Metadata JSON Schema", + "type": [ + "string", + "null" + ] + } + }, + "definitions": { + "Empty": { + "description": "An empty struct that serves as a placeholder in different places, such as contracts that don't set a custom message.\n\nIt is designed to be expressable in correct JSON and JSON Schema but contains no meaningful data. Previously we used enums without cases, but those cannot represented as valid JSON Schema (https://github.com/CosmWasm/cosmwasm/issues/451)", + "type": "object" + } + } +} diff --git a/wormhole/cw721-wrapped/schema/num_tokens_response.json b/wormhole/cw721-wrapped/schema/num_tokens_response.json new file mode 100644 index 00000000..4647c23a --- /dev/null +++ b/wormhole/cw721-wrapped/schema/num_tokens_response.json @@ -0,0 +1,15 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "NumTokensResponse", + "type": "object", + "required": [ + "count" + ], + "properties": { + "count": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + } +} diff --git a/wormhole/cw721-wrapped/schema/operators_response.json b/wormhole/cw721-wrapped/schema/operators_response.json new file mode 100644 index 00000000..53703072 --- /dev/null +++ b/wormhole/cw721-wrapped/schema/operators_response.json @@ -0,0 +1,97 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "OperatorsResponse", + "type": "object", + "required": [ + "operators" + ], + "properties": { + "operators": { + "type": "array", + "items": { + "$ref": "#/definitions/Approval" + } + } + }, + "definitions": { + "Approval": { + "type": "object", + "required": [ + "expires", + "spender" + ], + "properties": { + "expires": { + "description": "When the Approval expires (maybe Expiration::never)", + "allOf": [ + { + "$ref": "#/definitions/Expiration" + } + ] + }, + "spender": { + "description": "Account that can transfer/send the token", + "type": "string" + } + } + }, + "Expiration": { + "description": "Expiration represents a point in time when some event happens. It can compare with a BlockInfo and will return is_expired() == true once the condition is hit (and for every block in the future)", + "oneOf": [ + { + "description": "AtHeight will expire when `env.block.height` >= height", + "type": "object", + "required": [ + "at_height" + ], + "properties": { + "at_height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + { + "description": "AtTime will expire when `env.block.time` >= time", + "type": "object", + "required": [ + "at_time" + ], + "properties": { + "at_time": { + "$ref": "#/definitions/Timestamp" + } + }, + "additionalProperties": false + }, + { + "description": "Never will never expire. Used to express the empty variant", + "type": "object", + "required": [ + "never" + ], + "properties": { + "never": { + "type": "object" + } + }, + "additionalProperties": false + } + ] + }, + "Timestamp": { + "description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```", + "allOf": [ + { + "$ref": "#/definitions/Uint64" + } + ] + }, + "Uint64": { + "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```", + "type": "string" + } + } +} diff --git a/wormhole/cw721-wrapped/schema/owner_of_response.json b/wormhole/cw721-wrapped/schema/owner_of_response.json new file mode 100644 index 00000000..1258d671 --- /dev/null +++ b/wormhole/cw721-wrapped/schema/owner_of_response.json @@ -0,0 +1,103 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "OwnerOfResponse", + "type": "object", + "required": [ + "approvals", + "owner" + ], + "properties": { + "approvals": { + "description": "If set this address is approved to transfer/send the token as well", + "type": "array", + "items": { + "$ref": "#/definitions/Approval" + } + }, + "owner": { + "description": "Owner of the token", + "type": "string" + } + }, + "definitions": { + "Approval": { + "type": "object", + "required": [ + "expires", + "spender" + ], + "properties": { + "expires": { + "description": "When the Approval expires (maybe Expiration::never)", + "allOf": [ + { + "$ref": "#/definitions/Expiration" + } + ] + }, + "spender": { + "description": "Account that can transfer/send the token", + "type": "string" + } + } + }, + "Expiration": { + "description": "Expiration represents a point in time when some event happens. It can compare with a BlockInfo and will return is_expired() == true once the condition is hit (and for every block in the future)", + "oneOf": [ + { + "description": "AtHeight will expire when `env.block.height` >= height", + "type": "object", + "required": [ + "at_height" + ], + "properties": { + "at_height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + { + "description": "AtTime will expire when `env.block.time` >= time", + "type": "object", + "required": [ + "at_time" + ], + "properties": { + "at_time": { + "$ref": "#/definitions/Timestamp" + } + }, + "additionalProperties": false + }, + { + "description": "Never will never expire. Used to express the empty variant", + "type": "object", + "required": [ + "never" + ], + "properties": { + "never": { + "type": "object" + } + }, + "additionalProperties": false + } + ] + }, + "Timestamp": { + "description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```", + "allOf": [ + { + "$ref": "#/definitions/Uint64" + } + ] + }, + "Uint64": { + "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```", + "type": "string" + } + } +} diff --git a/wormhole/cw721-wrapped/schema/query_msg.json b/wormhole/cw721-wrapped/schema/query_msg.json new file mode 100644 index 00000000..cd3a9561 --- /dev/null +++ b/wormhole/cw721-wrapped/schema/query_msg.json @@ -0,0 +1,285 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "QueryMsg", + "oneOf": [ + { + "description": "Return the owner of the given token, error if token does not exist Return type: OwnerOfResponse", + "type": "object", + "required": [ + "owner_of" + ], + "properties": { + "owner_of": { + "type": "object", + "required": [ + "token_id" + ], + "properties": { + "include_expired": { + "description": "unset or false will filter out expired approvals, you must set to true to see them", + "type": [ + "boolean", + "null" + ] + }, + "token_id": { + "type": "string" + } + } + } + }, + "additionalProperties": false + }, + { + "description": "Return operator that can access all of the owner's tokens. Return type: `ApprovalResponse`", + "type": "object", + "required": [ + "approval" + ], + "properties": { + "approval": { + "type": "object", + "required": [ + "spender", + "token_id" + ], + "properties": { + "include_expired": { + "type": [ + "boolean", + "null" + ] + }, + "spender": { + "type": "string" + }, + "token_id": { + "type": "string" + } + } + } + }, + "additionalProperties": false + }, + { + "description": "Return approvals that a token has Return type: `ApprovalsResponse`", + "type": "object", + "required": [ + "approvals" + ], + "properties": { + "approvals": { + "type": "object", + "required": [ + "token_id" + ], + "properties": { + "include_expired": { + "type": [ + "boolean", + "null" + ] + }, + "token_id": { + "type": "string" + } + } + } + }, + "additionalProperties": false + }, + { + "description": "List all operators that can access all of the owner's tokens Return type: `OperatorsResponse`", + "type": "object", + "required": [ + "all_operators" + ], + "properties": { + "all_operators": { + "type": "object", + "required": [ + "owner" + ], + "properties": { + "include_expired": { + "description": "unset or false will filter out expired items, you must set to true to see them", + "type": [ + "boolean", + "null" + ] + }, + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "owner": { + "type": "string" + }, + "start_after": { + "type": [ + "string", + "null" + ] + } + } + } + }, + "additionalProperties": false + }, + { + "description": "Total number of tokens issued", + "type": "object", + "required": [ + "num_tokens" + ], + "properties": { + "num_tokens": { + "type": "object" + } + }, + "additionalProperties": false + }, + { + "description": "With MetaData Extension. Returns top-level metadata about the contract: `ContractInfoResponse`", + "type": "object", + "required": [ + "contract_info" + ], + "properties": { + "contract_info": { + "type": "object" + } + }, + "additionalProperties": false + }, + { + "description": "With MetaData Extension. Returns metadata about one particular token, based on *ERC721 Metadata JSON Schema* but directly from the contract: `NftInfoResponse`", + "type": "object", + "required": [ + "nft_info" + ], + "properties": { + "nft_info": { + "type": "object", + "required": [ + "token_id" + ], + "properties": { + "token_id": { + "type": "string" + } + } + } + }, + "additionalProperties": false + }, + { + "description": "With MetaData Extension. Returns the result of both `NftInfo` and `OwnerOf` as one query as an optimization for clients: `AllNftInfo`", + "type": "object", + "required": [ + "all_nft_info" + ], + "properties": { + "all_nft_info": { + "type": "object", + "required": [ + "token_id" + ], + "properties": { + "include_expired": { + "description": "unset or false will filter out expired approvals, you must set to true to see them", + "type": [ + "boolean", + "null" + ] + }, + "token_id": { + "type": "string" + } + } + } + }, + "additionalProperties": false + }, + { + "description": "With Enumerable extension. Returns all tokens owned by the given address, [] if unset. Return type: TokensResponse.", + "type": "object", + "required": [ + "tokens" + ], + "properties": { + "tokens": { + "type": "object", + "required": [ + "owner" + ], + "properties": { + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "owner": { + "type": "string" + }, + "start_after": { + "type": [ + "string", + "null" + ] + } + } + } + }, + "additionalProperties": false + }, + { + "description": "With Enumerable extension. Requires pagination. Lists all token_ids controlled by the contract. Return type: TokensResponse.", + "type": "object", + "required": [ + "all_tokens" + ], + "properties": { + "all_tokens": { + "type": "object", + "properties": { + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "type": [ + "string", + "null" + ] + } + } + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "minter" + ], + "properties": { + "minter": { + "type": "object" + } + }, + "additionalProperties": false + } + ] +} diff --git a/wormhole/cw721-wrapped/schema/tokens_response.json b/wormhole/cw721-wrapped/schema/tokens_response.json new file mode 100644 index 00000000..b8e3d75b --- /dev/null +++ b/wormhole/cw721-wrapped/schema/tokens_response.json @@ -0,0 +1,17 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "TokensResponse", + "type": "object", + "required": [ + "tokens" + ], + "properties": { + "tokens": { + "description": "Contains all token_ids in lexicographical ordering If there are more than `limit`, use `start_from` in future queries to achieve pagination.", + "type": "array", + "items": { + "type": "string" + } + } + } +} diff --git a/wormhole/cw721-wrapped/src/lib.rs b/wormhole/cw721-wrapped/src/lib.rs new file mode 100644 index 00000000..e5791164 --- /dev/null +++ b/wormhole/cw721-wrapped/src/lib.rs @@ -0,0 +1,181 @@ +pub mod msg; +pub mod state; + +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +pub use cosmwasm_std::to_binary; +use cosmwasm_std::Empty; + +#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug, Default)] +pub struct Trait { + pub display_type: Option, + pub trait_type: String, + pub value: String, +} + +pub type Extension = Option; + +pub type Cw721MetadataContract<'a> = cw721_base::Cw721Contract<'a, Extension, Empty>; +pub type ExecuteMsg = cw721_base::ExecuteMsg; + +#[cfg(not(feature = "library"))] +pub mod entry { + + use std::convert::TryInto; + + pub use crate::msg::QueryMsg; + use crate::msg::{InstantiateMsg, WrappedAssetInfoResponse}; + pub use crate::state::{wrapped_asset_info, wrapped_asset_info_read, WrappedAssetInfo}; + + use super::*; + + use cosmwasm_std::{ + entry_point, to_binary, Binary, CosmosMsg, Deps, DepsMut, Env, MessageInfo, Response, + StdError, StdResult, WasmMsg, + }; + use cw721::Cw721Query; + + // version info for migration info + const CONTRACT_NAME: &str = "crates.io:cw721-wrapped"; + const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); + + // This is a simple type to let us handle empty extensions + + // This makes a conscious choice on the various generics used by the contract + #[entry_point] + pub fn instantiate( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: InstantiateMsg, + ) -> StdResult { + let base = Cw721MetadataContract::default(); + + cw2::set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + + let contract_info = cw721::ContractInfoResponse { + name: msg.name, + symbol: msg.symbol, + }; + base.contract_info.save(deps.storage, &contract_info)?; + let minter = deps.api.addr_validate(&msg.minter)?; + base.minter.save(deps.storage, &minter)?; + + // save wrapped asset info + let data = + WrappedAssetInfo { + asset_chain: msg.asset_chain, + asset_address: msg.asset_address.to_vec().try_into().map_err( + |_err| -> StdError { + StdError::GenericErr { + msg: "WrongSize".to_string(), + } + }, + )?, + bridge: deps.api.addr_canonicalize(info.sender.as_str())?, + }; + wrapped_asset_info(deps.storage).save(&data)?; + + if let Some(mint_msg) = msg.mint { + execute(deps, env, info, ExecuteMsg::Mint(mint_msg)) + .map_err(|e| StdError::generic_err(format!("{}", e)))?; + } + + if let Some(hook) = msg.init_hook { + Ok( + Response::new().add_message(CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: hook.contract_addr, + msg: hook.msg, + funds: vec![], + })), + ) + } else { + Ok(Response::default()) + } + } + + #[entry_point] + pub fn execute( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: ExecuteMsg, + ) -> Result { + Cw721MetadataContract::default().execute(deps, env, info, msg) + } + + #[entry_point] + pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { + let base = Cw721MetadataContract::default(); + match msg { + QueryMsg::WrappedAssetInfo {} => to_binary(&query_wrapped_asset_info(deps)?), + QueryMsg::OwnerOf { + token_id, + include_expired, + } => { + to_binary(&base.owner_of(deps, env, token_id, include_expired.unwrap_or(false))?) + } + QueryMsg::Approval { + token_id, + spender, + include_expired, + } => to_binary(&base.approval( + deps, + env, + token_id, + spender, + include_expired.unwrap_or(false), + )?), + QueryMsg::Approvals { + token_id, + include_expired, + } => { + to_binary(&base.approvals(deps, env, token_id, include_expired.unwrap_or(false))?) + } + QueryMsg::AllOperators { + owner, + include_expired, + start_after, + limit, + } => to_binary(&base.operators( + deps, + env, + owner, + include_expired.unwrap_or(false), + start_after, + limit, + )?), + QueryMsg::NumTokens {} => to_binary(&base.num_tokens(deps)?), + QueryMsg::Tokens { + owner, + start_after, + limit, + } => to_binary(&base.tokens(deps, owner, start_after, limit)?), + QueryMsg::AllTokens { start_after, limit } => { + to_binary(&base.all_tokens(deps, start_after, limit)?) + } + QueryMsg::Minter {} => to_binary(&base.minter(deps)?), + QueryMsg::ContractInfo {} => to_binary(&base.contract_info(deps)?), + QueryMsg::NftInfo { token_id } => to_binary(&base.nft_info(deps, token_id)?), + QueryMsg::AllNftInfo { + token_id, + include_expired, + } => to_binary(&base.all_nft_info( + deps, + env, + token_id, + include_expired.unwrap_or(false), + )?), + } + } + + pub fn query_wrapped_asset_info(deps: Deps) -> StdResult { + let info = wrapped_asset_info_read(deps.storage).load()?; + Ok(WrappedAssetInfoResponse { + asset_chain: info.asset_chain, + asset_address: info.asset_address, + bridge: deps.api.addr_humanize(&info.bridge)?, + }) + } +} diff --git a/wormhole/cw721-wrapped/src/msg.rs b/wormhole/cw721-wrapped/src/msg.rs new file mode 100644 index 00000000..b8ab9e56 --- /dev/null +++ b/wormhole/cw721-wrapped/src/msg.rs @@ -0,0 +1,128 @@ +pub use cw721_base::MintMsg; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use cosmwasm_std::{Addr, Binary, Empty}; + +pub use cw721_base::msg::ExecuteMsg; + +type HumanAddr = String; + +/// The cw721_base crate allows an extension point which we don't care about +pub type NoExt = Option; + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +pub struct InstantiateMsg { + /// Name of the NFT contract + pub name: String, + /// Symbol of the NFT contract + pub symbol: String, + + /// Native chain of the NFT + pub asset_chain: u16, + + /// Native address of the NFT + pub asset_address: Binary, + + /// The minter is the only one who can create new NFTs. + /// This is designed for a base NFT that is controlled by an external program + /// or contract. You will likely replace this with custom logic in custom NFTs + pub minter: String, + + /// Mint a new NFT upon creation + pub mint: Option>, + + /// Generic callback - used to register the newly instantiated asset + pub init_hook: Option, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +pub struct InitHook { + pub msg: Binary, + pub contract_addr: HumanAddr, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum QueryMsg { + /// Generic information about the wrapped asset + WrappedAssetInfo {}, + + /// Return the owner of the given token, error if token does not exist + /// Return type: OwnerOfResponse + OwnerOf { + token_id: String, + /// unset or false will filter out expired approvals, you must set to true to see them + include_expired: Option, + }, + + /// Return operator that can access all of the owner's tokens. + /// Return type: `ApprovalResponse` + Approval { + token_id: String, + spender: String, + include_expired: Option, + }, + + /// Return approvals that a token has + /// Return type: `ApprovalsResponse` + Approvals { + token_id: String, + include_expired: Option, + }, + + /// List all operators that can access all of the owner's tokens + /// Return type: `OperatorsResponse` + AllOperators { + owner: String, + /// unset or false will filter out expired items, you must set to true to see them + include_expired: Option, + start_after: Option, + limit: Option, + }, + /// Total number of tokens issued + NumTokens {}, + + /// With MetaData Extension. + /// Returns top-level metadata about the contract: `ContractInfoResponse` + ContractInfo {}, + + /// Returns metadata about one particular token, based on *ERC721 Metadata JSON Schema* + /// but directly from the contract: `NftInfoResponse` + NftInfo { + token_id: String, + }, + + /// Returns the result of both `NftInfo` and `OwnerOf` as one query as an optimization + /// for clients: `AllNftInfo` + AllNftInfo { + token_id: String, + /// unset or false will filter out expired approvals, you must set to true to see them + include_expired: Option, + }, + + /// Returns all tokens owned by the given address, [] if unset. + /// Return type: TokensResponse. + Tokens { + owner: String, + start_after: Option, + limit: Option, + }, + + /// Requires pagination. Lists all token_ids controlled by the contract. + /// Return type: TokensResponse. + AllTokens { + start_after: Option, + limit: Option, + }, + + // Return the minter + Minter {}, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +pub struct WrappedAssetInfoResponse { + pub asset_chain: u16, // Asset chain id + pub asset_address: Binary, // Asset smart contract address in the original chain + pub bridge: Addr, // Bridge address, authorized to mint and burn wrapped tokens +} diff --git a/wormhole/cw721-wrapped/src/state.rs b/wormhole/cw721-wrapped/src/state.rs new file mode 100644 index 00000000..8e8f4854 --- /dev/null +++ b/wormhole/cw721-wrapped/src/state.rs @@ -0,0 +1,23 @@ +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use cosmwasm_std::{Binary, CanonicalAddr, Storage}; +use cosmwasm_storage::{singleton, singleton_read, ReadonlySingleton, Singleton}; + +pub const KEY_WRAPPED_ASSET: &[u8] = b"wrappedAsset"; + +// Created at initialization and reference original asset and bridge address +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +pub struct WrappedAssetInfo { + pub asset_chain: u16, // Asset chain id + pub asset_address: Binary, // Asset smart contract address on the original chain + pub bridge: CanonicalAddr, // Bridge address, authorized to mint and burn wrapped tokens +} + +pub fn wrapped_asset_info(storage: &mut dyn Storage) -> Singleton { + singleton(storage, KEY_WRAPPED_ASSET) +} + +pub fn wrapped_asset_info_read(storage: &dyn Storage) -> ReadonlySingleton { + singleton_read(storage, KEY_WRAPPED_ASSET) +} diff --git a/wormhole/mock-bridge-integration/.cargo/config b/wormhole/mock-bridge-integration/.cargo/config new file mode 100644 index 00000000..2d5cce4e --- /dev/null +++ b/wormhole/mock-bridge-integration/.cargo/config @@ -0,0 +1,5 @@ +[alias] +wasm = "build --release --target wasm32-unknown-unknown" +wasm-debug = "build --target wasm32-unknown-unknown" +unit-test = "test --lib --features backtraces" +integration-test = "test --test integration" \ No newline at end of file diff --git a/wormhole/mock-bridge-integration/Cargo.toml b/wormhole/mock-bridge-integration/Cargo.toml new file mode 100644 index 00000000..2aaeef86 --- /dev/null +++ b/wormhole/mock-bridge-integration/Cargo.toml @@ -0,0 +1,37 @@ +[package] +name = "mock-bridge-integration" +version = "0.1.0" +edition = "2018" +description = "Mock Bridge Integration for Transfer w/ Payload" + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +backtraces = ["cosmwasm-std/backtraces"] +# use library feature to disable all init/handle/query exports +library = [] + +[dependencies] +cosmwasm-std = { version = "0.16.0" } +cosmwasm-storage = { version = "0.16.0" } +schemars = "0.8.1" +serde = { version = "1.0.103", default-features = false, features = ["derive"] } +cw20 = "0.8.0" +cw20-base = { version = "0.8.0", features = ["library"] } +cw20-wrapped = { path = "../cw20-wrapped", features = ["library"] } +terraswap = "2.4.0" +thiserror = { version = "1.0.20" } +k256 = { version = "0.9.4", default-features = false, features = ["ecdsa"] } +sha3 = { version = "0.9.1", default-features = false } +generic-array = { version = "0.14.4" } +hex = "0.4.2" +lazy_static = "1.4.0" +bigint = "4" + +wormhole-bridge-terra = { path = "../wormhole", features = ["library"] } +token-bridge-terra = { path = "../token-bridge", features = ["library"] } + +[dev-dependencies] +cosmwasm-vm = { version = "0.16.0", default-features = false } +serde_json = "1.0" diff --git a/wormhole/mock-bridge-integration/src/contract.rs b/wormhole/mock-bridge-integration/src/contract.rs new file mode 100644 index 00000000..18efdeb7 --- /dev/null +++ b/wormhole/mock-bridge-integration/src/contract.rs @@ -0,0 +1,91 @@ +use cosmwasm_std::{ + entry_point, to_binary, Binary, CosmosMsg, Deps, DepsMut, Env, MessageInfo, QueryRequest, + Reply, Response, StdError, StdResult, SubMsg, WasmMsg, WasmQuery, +}; + +use crate::msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}; +use crate::state::{config, config_read, Config}; + +use token_bridge_terra::msg::{ + ExecuteMsg as TokenBridgeExecuteMsg, QueryMsg as TokenBridgeQueryMessage, TransferInfoResponse, +}; + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn migrate(_deps: DepsMut, _env: Env, _msg: MigrateMsg) -> StdResult { + Ok(Response::new()) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn instantiate( + deps: DepsMut, + _env: Env, + _info: MessageInfo, + msg: InstantiateMsg, +) -> StdResult { + let state = Config { + token_bridge_contract: msg.token_bridge_contract, + }; + config(deps.storage).save(&state)?; + + Ok(Response::default()) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn execute(deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg) -> StdResult { + match msg { + ExecuteMsg::CompleteTransferWithPayload { data } => { + complete_transfer_with_payload(deps, env, info, &data) + } + } +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn reply(_deps: DepsMut, _env: Env, _msg: Reply) -> StdResult { + Ok(Response::default()) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn query(_deps: Deps, _env: Env, _msg: QueryMsg) -> StdResult { + Err(StdError::generic_err("not implemented")) +} + +fn complete_transfer_with_payload( + deps: DepsMut, + _env: Env, + info: MessageInfo, + data: &Binary, +) -> StdResult { + let cfg = config_read(deps.storage).load()?; + + let messages = vec![SubMsg::reply_on_success( + CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: cfg.token_bridge_contract, + msg: to_binary(&TokenBridgeExecuteMsg::CompleteTransferWithPayload { + data: data.clone(), + relayer: info.sender.to_string(), + })?, + funds: vec![], + }), + 1, + )]; + + let transfer_info = parse_transfer_vaa(deps.as_ref(), data)?; + + Ok(Response::new() + .add_submessages(messages) + .add_attribute("action", "complete_transfer_with_payload") + .add_attribute( + "transfer_payload", + Binary::from(transfer_info.payload).to_base64(), + )) +} + +fn parse_transfer_vaa(deps: Deps, data: &Binary) -> StdResult { + let cfg = config_read(deps.storage).load()?; + let transfer_info: TransferInfoResponse = + deps.querier.query(&QueryRequest::Wasm(WasmQuery::Smart { + contract_addr: cfg.token_bridge_contract, + msg: to_binary(&TokenBridgeQueryMessage::TransferInfo { vaa: data.clone() })?, + }))?; + Ok(transfer_info) +} diff --git a/wormhole/mock-bridge-integration/src/lib.rs b/wormhole/mock-bridge-integration/src/lib.rs new file mode 100644 index 00000000..f67a6e5e --- /dev/null +++ b/wormhole/mock-bridge-integration/src/lib.rs @@ -0,0 +1,6 @@ +#[cfg(test)] +extern crate lazy_static; + +pub mod contract; +pub mod msg; +pub mod state; diff --git a/wormhole/mock-bridge-integration/src/msg.rs b/wormhole/mock-bridge-integration/src/msg.rs new file mode 100644 index 00000000..e5162763 --- /dev/null +++ b/wormhole/mock-bridge-integration/src/msg.rs @@ -0,0 +1,26 @@ +use cosmwasm_std::Binary; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +type HumanAddr = String; + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +pub struct InstantiateMsg { + pub token_bridge_contract: HumanAddr, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum ExecuteMsg { + CompleteTransferWithPayload { data: Binary }, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub struct MigrateMsg {} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum QueryMsg { + WrappedRegistry { chain: u16, address: Binary }, +} diff --git a/wormhole/mock-bridge-integration/src/state.rs b/wormhole/mock-bridge-integration/src/state.rs new file mode 100644 index 00000000..95152b6a --- /dev/null +++ b/wormhole/mock-bridge-integration/src/state.rs @@ -0,0 +1,23 @@ +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use cosmwasm_std::Storage; +use cosmwasm_storage::{singleton, singleton_read, ReadonlySingleton, Singleton}; + +type HumanAddr = String; + +pub static CONFIG_KEY: &[u8] = b"config"; + +// Guardian set information +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +pub struct Config { + pub token_bridge_contract: HumanAddr, +} + +pub fn config(storage: &mut dyn Storage) -> Singleton { + singleton(storage, CONFIG_KEY) +} + +pub fn config_read(storage: &dyn Storage) -> ReadonlySingleton { + singleton_read(storage, CONFIG_KEY) +} diff --git a/wormhole/nft-bridge/.cargo/config b/wormhole/nft-bridge/.cargo/config new file mode 100644 index 00000000..2d5cce4e --- /dev/null +++ b/wormhole/nft-bridge/.cargo/config @@ -0,0 +1,5 @@ +[alias] +wasm = "build --release --target wasm32-unknown-unknown" +wasm-debug = "build --target wasm32-unknown-unknown" +unit-test = "test --lib --features backtraces" +integration-test = "test --test integration" \ No newline at end of file diff --git a/wormhole/nft-bridge/Cargo.toml b/wormhole/nft-bridge/Cargo.toml new file mode 100644 index 00000000..22331b61 --- /dev/null +++ b/wormhole/nft-bridge/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "nft-bridge" +version = "0.1.0" +edition = "2018" +description = "Wormhole NFT bridge" + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +backtraces = ["cosmwasm-std/backtraces"] +# use library feature to disable all init/handle/query exports +library = [] + +[dependencies] +cosmwasm-std = "1.0.0" +cosmwasm-storage = { version = "1.0.0" } +cw721 = { path = "../../packages/cw721" } +cw721-base = { path = "../cw721-base", version = "0.10.0", features = ["library"] } +cw721-wrapped = { path = "../cw721-wrapped", features = ["library"] } +hex = "0.4.2" +schemars = "0.8.10" +serde = { version = "1.0.137", default-features = false, features = ["derive"] } +sha3 = { version = "0.9.1", default-features = false } +wormhole-bridge-terra = { path = "../wormhole", features = ["library"] } + +[dev-dependencies] +serde_json = "1.0" +lazy_static = "1.4.0" diff --git a/wormhole/nft-bridge/src/contract.rs b/wormhole/nft-bridge/src/contract.rs new file mode 100644 index 00000000..b3846196 --- /dev/null +++ b/wormhole/nft-bridge/src/contract.rs @@ -0,0 +1,531 @@ +use crate::msg::WrappedRegistryResponse; +use crate::state::{spl_cache, spl_cache_read, wrapped_asset, BoundedVec, SplCacheItem}; +use crate::token_id::{from_external_token_id, to_external_token_id}; +use crate::CHAIN_ID; +use cosmwasm_std::{ + entry_point, to_binary, Binary, CanonicalAddr, CosmosMsg, Deps, DepsMut, Empty, Env, + MessageInfo, Order, QueryRequest, Response, StdError, StdResult, WasmMsg, WasmQuery, +}; + +use crate::msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}; +use crate::state::{ + bridge_contracts, bridge_contracts_read, config, config_read, wrapped_asset_address, + wrapped_asset_address_read, wrapped_asset_read, Action, ConfigInfo, RegisterChain, + TokenBridgeMessage, TransferInfo, UpgradeContract, +}; +use wormhole::byte_utils::{ + extend_address_to_32, extend_address_to_32_array, get_string_from_32, string_to_array, + ByteUtils, +}; +use wormhole::error::ContractError; + +use wormhole::msg::{ExecuteMsg as WormholeExecuteMsg, QueryMsg as WormholeQueryMsg}; + +use wormhole::state::{vaa_archive_add, vaa_archive_check, GovernancePacket, ParsedVAA}; + +use sha3::{Digest, Keccak256}; + +type HumanAddr = String; + +const WRAPPED_ASSET_UPDATING: &str = "updating"; + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn migrate(_deps: DepsMut, _env: Env, _msg: MigrateMsg) -> StdResult { + Ok(Response::new()) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn instantiate( + deps: DepsMut, + _env: Env, + _info: MessageInfo, + msg: InstantiateMsg, +) -> StdResult { + // Save general wormhole info + let state = ConfigInfo { + gov_chain: msg.gov_chain, + gov_address: msg.gov_address.as_slice().to_vec(), + wormhole_contract: msg.wormhole_contract, + wrapped_asset_code_id: msg.wrapped_asset_code_id, + }; + config(deps.storage).save(&state)?; + + Ok(Response::default()) +} + +pub fn parse_vaa(deps: DepsMut, block_time: u64, data: &Binary) -> StdResult { + let cfg = config_read(deps.storage).load()?; + let vaa: ParsedVAA = deps.querier.query(&QueryRequest::Wasm(WasmQuery::Smart { + contract_addr: cfg.wormhole_contract, + msg: to_binary(&WormholeQueryMsg::VerifyVAA { + vaa: data.clone(), + block_time, + })?, + }))?; + Ok(vaa) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn execute(deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg) -> StdResult { + match msg { + ExecuteMsg::InitiateTransfer { + contract_addr, + token_id, + recipient_chain, + recipient, + nonce, + } => handle_initiate_transfer( + deps, + env, + info, + contract_addr, + token_id, + recipient_chain, + recipient.to_array()?, + nonce, + ), + ExecuteMsg::SubmitVaa { data } => submit_vaa(deps, env, info, &data), + ExecuteMsg::RegisterAssetHook { asset_id } => { + handle_register_asset(deps, env, info, asset_id.as_slice()) + } + } +} + +fn submit_vaa( + mut deps: DepsMut, + env: Env, + info: MessageInfo, + data: &Binary, +) -> StdResult { + let state = config_read(deps.storage).load()?; + + let vaa = parse_vaa(deps.branch(), env.block.time.seconds(), data)?; + let data = vaa.payload; + + if vaa_archive_check(deps.storage, vaa.hash.as_slice()) { + return ContractError::VaaAlreadyExecuted.std_err(); + } + vaa_archive_add(deps.storage, vaa.hash.as_slice())?; + + // check if vaa is from governance + if state.gov_chain == vaa.emitter_chain && state.gov_address == vaa.emitter_address { + return handle_governance_payload(deps, env, &data); + } + + let message = TokenBridgeMessage::deserialize(&data)?; + + match message.action { + Action::TRANSFER => handle_complete_transfer( + deps, + env, + info, + vaa.emitter_chain, + vaa.emitter_address, + TransferInfo::deserialize(&message.payload)?, + ), + _ => ContractError::InvalidVAAAction.std_err(), + } +} + +fn handle_governance_payload(deps: DepsMut, env: Env, data: &[u8]) -> StdResult { + let gov_packet = GovernancePacket::deserialize(data)?; + let module = get_string_from_32(&gov_packet.module); + + if module != "NFTBridge" { + return Err(StdError::generic_err("this is not a valid module")); + } + + if gov_packet.chain != 0 && gov_packet.chain != CHAIN_ID { + return Err(StdError::generic_err( + "the governance VAA is for another chain", + )); + } + + match gov_packet.action { + 1u8 => handle_register_chain(deps, env, RegisterChain::deserialize(&gov_packet.payload)?), + 2u8 => handle_upgrade_contract( + deps, + env, + UpgradeContract::deserialize(&gov_packet.payload)?, + ), + _ => ContractError::InvalidVAAAction.std_err(), + } +} + +fn handle_upgrade_contract( + _deps: DepsMut, + env: Env, + upgrade_contract: UpgradeContract, +) -> StdResult { + Ok(Response::new() + .add_message(CosmosMsg::Wasm(WasmMsg::Migrate { + contract_addr: env.contract.address.to_string(), + new_code_id: upgrade_contract.new_contract, + msg: to_binary(&MigrateMsg {})?, + })) + .add_attribute("action", "contract_upgrade")) +} + +fn handle_register_chain( + deps: DepsMut, + _env: Env, + register_chain: RegisterChain, +) -> StdResult { + let RegisterChain { + chain_id, + chain_address, + } = register_chain; + + let existing = bridge_contracts_read(deps.storage).load(&chain_id.to_be_bytes()); + if existing.is_ok() { + return Err(StdError::generic_err( + "bridge contract already exists for this chain", + )); + } + + let mut bucket = bridge_contracts(deps.storage); + bucket.save(&chain_id.to_be_bytes(), &chain_address)?; + + Ok(Response::new() + .add_attribute("chain_id", chain_id.to_string()) + .add_attribute("chain_address", hex::encode(chain_address))) +} + +fn handle_complete_transfer( + deps: DepsMut, + env: Env, + _info: MessageInfo, + emitter_chain: u16, + emitter_address: Vec, + transfer_info: TransferInfo, +) -> StdResult { + let cfg = config_read(deps.storage).load()?; + + let expected_contract = + bridge_contracts_read(deps.storage).load(&emitter_chain.to_be_bytes())?; + + // must be sent by a registered token bridge contract + if expected_contract != emitter_address { + return Err(StdError::generic_err("invalid emitter")); + } + + if transfer_info.recipient_chain != CHAIN_ID { + return Err(StdError::generic_err( + "this transfer is not directed at this chain", + )); + } + + let token_chain = transfer_info.nft_chain; + let target_address = &(&transfer_info.recipient[..]).get_address(0); + + let mut messages = vec![]; + + let recipient = deps + .api + .addr_humanize(target_address) + .or_else(|_| ContractError::WrongTargetAddressFormat.std_err())?; + + let contract_addr; + + let token_id = from_external_token_id( + deps.storage, + token_chain, + &transfer_info.nft_address, + &transfer_info.token_id, + )?; + + if token_chain != CHAIN_ID { + // NFT is not native to this chain, so we need a wrapper + let asset_address = transfer_info.nft_address; + let asset_id = build_asset_id(token_chain, &asset_address); + + let token_uri = String::from_utf8(transfer_info.uri.to_vec()) + .map_err(|_| StdError::generic_err("could not parse uri string"))?; + + let mint_msg = cw721_base::msg::MintMsg { + token_id, + owner: recipient.to_string(), + token_uri: Some(token_uri), + extension: None, + }; + + // Check if this asset is already deployed + if let Ok(wrapped_addr) = wrapped_asset_read(deps.storage).load(&asset_id) { + contract_addr = wrapped_addr; + // Asset already deployed, just mint + + messages.push(CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: contract_addr.clone(), + msg: to_binary(&cw721_base::msg::ExecuteMsg::Mint(mint_msg))?, + funds: vec![], + })); + } else { + contract_addr = env.contract.address.clone().into_string(); + wrapped_asset(deps.storage) + .save(&asset_id, &HumanAddr::from(WRAPPED_ASSET_UPDATING))?; + + let (name, symbol) = if token_chain == 1 { + let spl_cache_item = SplCacheItem { + name: transfer_info.name, + symbol: transfer_info.symbol, + }; + spl_cache(deps.storage).save(&transfer_info.token_id, &spl_cache_item)?; + // Solana NFTs all use the same NFT contract, so unify the name + ( + "Wormhole Bridged Solana-NFT".to_string(), + "WORMSPLNFT".to_string(), + ) + } else { + ( + get_string_from_32(&transfer_info.name), + get_string_from_32(&transfer_info.symbol), + ) + }; + messages.push(CosmosMsg::Wasm(WasmMsg::Instantiate { + admin: Some(contract_addr.clone()), + code_id: cfg.wrapped_asset_code_id, + msg: to_binary(&cw721_wrapped::msg::InstantiateMsg { + name, + symbol, + asset_chain: token_chain, + asset_address: (&transfer_info.nft_address[..]).into(), + minter: env.contract.address.into_string(), + mint: Some(mint_msg), + init_hook: Some(cw721_wrapped::msg::InitHook { + msg: cw721_wrapped::to_binary(&ExecuteMsg::RegisterAssetHook { + asset_id: asset_id.to_vec().into(), + }) + .map_err(|_| StdError::generic_err("couldn't convert to binary"))?, + contract_addr: contract_addr.clone(), + }), + })?, + funds: vec![], + label: String::new(), + })); + } + } else { + // Native NFT, transfer from custody + let token_address = (&transfer_info.nft_address[..]).get_address(0); + + contract_addr = deps.api.addr_humanize(&token_address)?.to_string(); + + messages.push(CosmosMsg::::Wasm(WasmMsg::Execute { + contract_addr: contract_addr.clone(), + msg: to_binary(&cw721_base::msg::ExecuteMsg::>::TransferNft { + recipient: recipient.to_string(), + token_id, + })?, + funds: vec![], + })); + } + Ok(Response::new() + .add_messages(messages) + .add_attribute("action", "complete_transfer") + .add_attribute("recipient", recipient) + .add_attribute("contract", contract_addr)) +} + +#[allow(clippy::too_many_arguments)] +fn handle_initiate_transfer( + deps: DepsMut, + env: Env, + info: MessageInfo, + asset: HumanAddr, + token_id: String, + recipient_chain: u16, + recipient: [u8; 32], + nonce: u32, +) -> StdResult { + if recipient_chain == CHAIN_ID { + return ContractError::SameSourceAndTarget.std_err(); + } + + let asset_chain: u16; + let asset_address: [u8; 32]; + + let cfg: ConfigInfo = config_read(deps.storage).load()?; + let asset_canonical: CanonicalAddr = deps.api.addr_canonicalize(&asset)?; + + let mut messages: Vec = vec![]; + + if wrapped_asset_address_read(deps.storage) + .load(asset_canonical.as_slice()) + .is_ok() + { + // This is a deployed wrapped asset, burn it + messages.push(CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: asset.clone(), + msg: to_binary(&cw721_wrapped::msg::ExecuteMsg::Burn::> { + token_id: token_id.clone(), + })?, + funds: vec![], + })); + + let wrapped_token_info: cw721_wrapped::msg::WrappedAssetInfoResponse = + deps.querier + .query(&QueryRequest::::Wasm(WasmQuery::Smart { + contract_addr: asset.clone(), + msg: to_binary(&cw721_wrapped::msg::QueryMsg::WrappedAssetInfo {})?, + }))?; + + asset_address = wrapped_token_info.asset_address.to_array()?; + asset_chain = wrapped_token_info.asset_chain; + } else { + // Native NFT, lock it up + messages.push(CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: asset.clone(), + msg: to_binary(&cw721_base::msg::ExecuteMsg::>::TransferNft { + recipient: env.contract.address.to_string(), + token_id: token_id.clone(), + })?, + funds: vec![], + })); + + asset_chain = CHAIN_ID; + asset_address = extend_address_to_32_array(&asset_canonical); + }; + + let external_token_id = + to_external_token_id(deps.storage, asset_chain, &asset_address, token_id.clone())?; + + let symbol: [u8; 32]; + let name: [u8; 32]; + + if asset_chain == 1 { + let SplCacheItem { + name: cached_name, + symbol: cached_symbol, + } = spl_cache_read(deps.storage).load(&external_token_id)?; + symbol = cached_symbol; + name = cached_name; + } else { + let response: cw721::ContractInfoResponse = + deps.querier + .query(&QueryRequest::::Wasm(WasmQuery::Smart { + contract_addr: asset.clone(), + msg: to_binary(&cw721_base::msg::QueryMsg::ContractInfo {})?, + }))?; + name = string_to_array(&response.name); + symbol = string_to_array(&response.symbol); + } + + let cw721::NftInfoResponse::> { token_uri, .. } = + deps.querier + .query(&QueryRequest::::Wasm(WasmQuery::Smart { + contract_addr: asset, + msg: to_binary(&cw721_base::msg::QueryMsg::NftInfo { + token_id: token_id.clone(), + })?, + }))?; + + let transfer_info = TransferInfo { + nft_address: asset_address, + nft_chain: asset_chain, + symbol, + name, + token_id: external_token_id, + uri: BoundedVec::new(token_uri.unwrap().into())?, + recipient, + recipient_chain, + }; + + let token_bridge_message = TokenBridgeMessage { + action: Action::TRANSFER, + payload: transfer_info.serialize(), + }; + + messages.push(CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: cfg.wormhole_contract, + msg: to_binary(&WormholeExecuteMsg::PostMessage { + message: Binary::from(token_bridge_message.serialize()), + nonce, + })?, + funds: vec![], + })); + + Ok(Response::new() + .add_messages(messages) + .add_attribute("transfer.token_chain", asset_chain.to_string()) + .add_attribute("transfer.token", hex::encode(asset_address)) + .add_attribute("transfer.token_id", token_id) + .add_attribute("transfer.external_token_id", hex::encode(external_token_id)) + .add_attribute( + "transfer.sender", + hex::encode(extend_address_to_32( + &deps.api.addr_canonicalize(info.sender.as_str())?, + )), + ) + .add_attribute("transfer.recipient_chain", recipient_chain.to_string()) + .add_attribute("transfer.recipient", hex::encode(recipient)) + .add_attribute("transfer.nonce", nonce.to_string()) + .add_attribute("transfer.block_time", env.block.time.seconds().to_string())) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { + match msg { + QueryMsg::WrappedRegistry { chain, address } => { + to_binary(&query_wrapped_registry(deps, chain, address.as_slice())?) + } + QueryMsg::AllWrappedAssets {} => to_binary(&query_all_wrapped_assets(deps)?), + } +} + +/// Handle wrapped asset registration messages +fn handle_register_asset( + deps: DepsMut, + _env: Env, + info: MessageInfo, + asset_id: &[u8], +) -> StdResult { + let mut bucket = wrapped_asset(deps.storage); + let result = bucket + .load(asset_id) + .map_err(|_| ContractError::RegistrationForbidden.std())?; + if result != WRAPPED_ASSET_UPDATING { + return ContractError::AssetAlreadyRegistered.std_err(); + } + + bucket.save(asset_id, &info.sender.to_string())?; + + let contract_address: CanonicalAddr = deps.api.addr_canonicalize(info.sender.as_str())?; + wrapped_asset_address(deps.storage).save(contract_address.as_slice(), &asset_id.to_vec())?; + + Ok(Response::new() + .add_attribute("action", "register_asset") + .add_attribute("asset_id", format!("{:?}", asset_id)) + .add_attribute("contract_addr", info.sender)) +} + +pub fn query_wrapped_registry( + deps: Deps, + chain: u16, + address: &[u8], +) -> StdResult { + let asset_id = build_asset_id(chain, address); + // Check if this asset is already deployed + match wrapped_asset_read(deps.storage).load(&asset_id) { + Ok(address) => Ok(WrappedRegistryResponse { address }), + Err(_) => ContractError::AssetNotFound.std_err(), + } +} + +fn query_all_wrapped_assets(deps: Deps) -> StdResult> { + let bucket = wrapped_asset_address_read(deps.storage); + let mut result = vec![]; + for item in bucket.range(None, None, Order::Ascending) { + let contract_address = item?.0.into(); + result.push(deps.api.addr_humanize(&contract_address)?.to_string()) + } + Ok(result) +} + +fn build_asset_id(chain: u16, address: &[u8]) -> Vec { + let mut asset_id: Vec = vec![]; + asset_id.extend_from_slice(&chain.to_be_bytes()); + asset_id.extend_from_slice(address); + + let mut hasher = Keccak256::new(); + hasher.update(asset_id); + hasher.finalize().to_vec() +} diff --git a/wormhole/nft-bridge/src/lib.rs b/wormhole/nft-bridge/src/lib.rs new file mode 100644 index 00000000..6aab89fc --- /dev/null +++ b/wormhole/nft-bridge/src/lib.rs @@ -0,0 +1,7 @@ +pub mod contract; +pub mod msg; +pub mod state; +pub mod token_id; + +// Chain ID of Terra +const CHAIN_ID: u16 = 3; diff --git a/wormhole/nft-bridge/src/msg.rs b/wormhole/nft-bridge/src/msg.rs new file mode 100644 index 00000000..b6adf4e5 --- /dev/null +++ b/wormhole/nft-bridge/src/msg.rs @@ -0,0 +1,58 @@ +use cosmwasm_std::Binary; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +type HumanAddr = String; + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +pub struct InstantiateMsg { + // governance contract details + pub gov_chain: u16, + pub gov_address: Binary, + + pub wormhole_contract: HumanAddr, + pub wrapped_asset_code_id: u64, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum ExecuteMsg { + RegisterAssetHook { + asset_id: Binary, + }, + + InitiateTransfer { + contract_addr: String, + token_id: String, + recipient_chain: u16, + recipient: Binary, + nonce: u32, + }, + + SubmitVaa { + data: Binary, + }, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub struct MigrateMsg {} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum QueryMsg { + WrappedRegistry { chain: u16, address: Binary }, + AllWrappedAssets {}, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub struct WrappedRegistryResponse { + pub address: HumanAddr, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum WormholeQueryMsg { + VerifyVAA { vaa: Binary, block_time: u64 }, +} diff --git a/wormhole/nft-bridge/src/state.rs b/wormhole/nft-bridge/src/state.rs new file mode 100644 index 00000000..28db7d53 --- /dev/null +++ b/wormhole/nft-bridge/src/state.rs @@ -0,0 +1,259 @@ +use std::convert::TryInto; + +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use cosmwasm_std::{StdError, StdResult, Storage}; +use cosmwasm_storage::{ + bucket, bucket_read, singleton, singleton_read, Bucket, ReadonlyBucket, ReadonlySingleton, + Singleton, +}; + +use wormhole::byte_utils::ByteUtils; + +type HumanAddr = String; + +pub static CONFIG_KEY: &[u8] = b"config"; +pub static WRAPPED_ASSET_KEY: &[u8] = b"wrapped_asset"; +pub static WRAPPED_ASSET_ADDRESS_KEY: &[u8] = b"wrapped_asset_address"; +pub static BRIDGE_CONTRACTS_KEY: &[u8] = b"bridge_contracts"; +pub static TOKEN_ID_HASHES_KEY: &[u8] = b"token_id_hashes"; +pub static SPL_CACHE_KEY: &[u8] = b"spl_cache"; + +// Guardian set information +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +pub struct ConfigInfo { + // governance contract details + pub gov_chain: u16, + pub gov_address: Vec, + + pub wormhole_contract: HumanAddr, + pub wrapped_asset_code_id: u64, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +pub struct SplCacheItem { + pub name: [u8; 32], + pub symbol: [u8; 32], +} + +pub fn config(storage: &mut dyn Storage) -> Singleton { + singleton(storage, CONFIG_KEY) +} + +pub fn config_read(storage: &dyn Storage) -> ReadonlySingleton { + singleton_read(storage, CONFIG_KEY) +} + +pub fn bridge_contracts(storage: &mut dyn Storage) -> Bucket> { + bucket(storage, BRIDGE_CONTRACTS_KEY) +} + +pub fn bridge_contracts_read(storage: &dyn Storage) -> ReadonlyBucket> { + bucket_read(storage, BRIDGE_CONTRACTS_KEY) +} + +pub fn wrapped_asset(storage: &mut dyn Storage) -> Bucket { + bucket(storage, WRAPPED_ASSET_KEY) +} + +pub fn wrapped_asset_read(storage: &dyn Storage) -> ReadonlyBucket { + bucket_read(storage, WRAPPED_ASSET_KEY) +} + +pub fn wrapped_asset_address(storage: &mut dyn Storage) -> Bucket> { + bucket(storage, WRAPPED_ASSET_ADDRESS_KEY) +} + +pub fn wrapped_asset_address_read(storage: &dyn Storage) -> ReadonlyBucket> { + bucket_read(storage, WRAPPED_ASSET_ADDRESS_KEY) +} + +pub fn spl_cache(storage: &mut dyn Storage) -> Bucket { + bucket(storage, SPL_CACHE_KEY) +} + +pub fn spl_cache_read(storage: &dyn Storage) -> ReadonlyBucket { + bucket_read(storage, SPL_CACHE_KEY) +} + +pub fn token_id_hashes(storage: &mut dyn Storage, chain: u16, address: [u8; 32]) -> Bucket { + Bucket::multilevel( + storage, + &[TOKEN_ID_HASHES_KEY, &chain.to_be_bytes(), &address], + ) +} + +pub fn token_id_hashes_read( + storage: &mut dyn Storage, + chain: u16, + address: [u8; 32], +) -> ReadonlyBucket { + ReadonlyBucket::multilevel( + storage, + &[TOKEN_ID_HASHES_KEY, &chain.to_be_bytes(), &address], + ) +} + +pub struct Action; + +impl Action { + pub const TRANSFER: u8 = 1; +} + +// 0 u8 action +// 1 [u8] payload +pub struct TokenBridgeMessage { + pub action: u8, + pub payload: Vec, +} + +impl TokenBridgeMessage { + pub fn deserialize(data: &[u8]) -> StdResult { + let action = data.get_u8(0); + let payload = &data[1..]; + + Ok(TokenBridgeMessage { + action, + payload: payload.to_vec(), + }) + } + + pub fn serialize(&self) -> Vec { + [self.action.to_be_bytes().to_vec(), self.payload.clone()].concat() + } +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +#[repr(transparent)] +pub struct BoundedVec { + vec: Vec, +} + +impl BoundedVec { + pub fn new(vec: Vec) -> StdResult { + if vec.len() > N { + return Result::Err(StdError::GenericErr { + msg: format!("vector length exceeds {}", N), + }); + }; + Ok(Self { vec }) + } + + #[inline] + pub fn to_vec(&self) -> Vec + where + T: Clone, + { + self.vec.clone() + } + + #[inline] + pub fn len(&self) -> usize { + self.vec.len() + } + + #[inline] + pub fn is_empty(&self) -> bool { + self.vec.is_empty() + } +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +pub struct TransferInfo { + pub nft_address: [u8; 32], + pub nft_chain: u16, + pub symbol: [u8; 32], + pub name: [u8; 32], + pub token_id: [u8; 32], + pub uri: BoundedVec, // max 200 bytes due to Solana + pub recipient: [u8; 32], + pub recipient_chain: u16, +} + +impl TransferInfo { + pub fn deserialize(data: &[u8]) -> StdResult { + let mut offset: usize = 0; // offset into data in bytes + let nft_address = data.get_const_bytes::<32>(offset); + offset += 32; + let nft_chain = data.get_u16(offset); + offset += 2; + let symbol = data.get_const_bytes::<32>(offset); + offset += 32; + let name = data.get_const_bytes::<32>(offset); + offset += 32; + let token_id = data.get_const_bytes::<32>(offset); + offset += 32; + let uri_length: usize = data.get_u8(offset).into(); + offset += 1; + let uri = data.get_bytes(offset, uri_length).to_vec(); + offset += uri_length; + let recipient = data.get_const_bytes::<32>(offset); + offset += 32; + let recipient_chain = data.get_u16(offset); + offset += 2; + + if data.len() != offset { + return Result::Err(StdError::GenericErr { + msg: format!( + "Invalid transfer length, expected {}, but got {}", + offset, + data.len() + ), + }); + } + + Ok(TransferInfo { + nft_address, + nft_chain, + symbol, + name, + token_id, + uri: BoundedVec::new(uri.to_vec())?, + recipient, + recipient_chain, + }) + } + pub fn serialize(&self) -> Vec { + [ + self.nft_address.to_vec(), + self.nft_chain.to_be_bytes().to_vec(), + self.symbol.to_vec(), + self.name.to_vec(), + self.token_id.to_vec(), + vec![self.uri.len().try_into().unwrap()], // won't panic, because uri.len() is less than 200 + self.uri.to_vec(), + self.recipient.to_vec(), + self.recipient_chain.to_be_bytes().to_vec(), + ] + .concat() + } +} + +pub struct UpgradeContract { + pub new_contract: u64, +} + +pub struct RegisterChain { + pub chain_id: u16, + pub chain_address: Vec, +} + +impl UpgradeContract { + pub fn deserialize(data: &[u8]) -> StdResult { + let new_contract = data.get_u64(24); + Ok(UpgradeContract { new_contract }) + } +} + +impl RegisterChain { + pub fn deserialize(data: &[u8]) -> StdResult { + let chain_id = data.get_u16(0); + let chain_address = data[2..].to_vec(); + + Ok(RegisterChain { + chain_id, + chain_address, + }) + } +} diff --git a/wormhole/nft-bridge/src/token_id.rs b/wormhole/nft-bridge/src/token_id.rs new file mode 100644 index 00000000..094188a5 --- /dev/null +++ b/wormhole/nft-bridge/src/token_id.rs @@ -0,0 +1,78 @@ +use std::str::FromStr; + +use cosmwasm_std::{StdResult, Storage, Uint256}; +use sha3::digest::consts::U32; +use sha3::digest::generic_array::GenericArray; +use sha3::{Digest, Keccak256}; +use wormhole::byte_utils::ByteUtils; + +use crate::state::{token_id_hashes, token_id_hashes_read}; +use crate::CHAIN_ID; + +// NOTE: [External and internal token id conversion] +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// +// The CW721 NFT standard allows token ids to be arbitrarily long (utf8) +// strings, while the token_ids in VAA payloads are always 32 bytes (and not +// necessarily valid utf8). +// +// We call a token id that's in string format an "internal id", and a token id +// that's in 32 byte format an "external id". Note that whether a token id is in +// internal format or external format doesn't imply which chain the token id +// originates from. We can have a terra (native) token id in both internal and +// external formats, and likewise we can have an ethereum token in in both +// internal and external formats. +// +// To support seamless transfers through the bridge, we need a way to have a +// 1-to-1 mapping from internal ids to external ids. +// When a foreign (such as ethereum or solana) token id first comes through, we +// simply render it into a string by formatting it as a decimal number. Then, +// when we want to transfer such a token back through the bridge, we simply +// parse the string back into a u256 (32 byte) number. +// +// When a native token id first leaves through the bridge, we turn its id into a +// 32 byte hash (keccak256). This hash is the external id. We store a mapping +// +// (chain_id, nft_address, keccak256(internal_id)) => internal_id +// +// so that we can turn it back into an internal id when it comes back through +// the bridge. When the token is sent back, we could choose to delete the hash +// from the store, but we do not. This way, external token verifiers will be +// able to verify NFT origins even for NFTs that have been transferred back. +// +// If two token ids within the same contract have the same keccak256 hash, then +// it's possible to lose tokens, but this is very unlikely. + +pub fn from_external_token_id( + storage: &mut dyn Storage, + nft_chain: u16, + nft_address: &[u8; 32], + token_id_external: &[u8; 32], +) -> StdResult { + if nft_chain == CHAIN_ID { + token_id_hashes_read(storage, nft_chain, *nft_address).load(token_id_external) + } else { + Ok(format!("{}", Uint256::from_be_bytes(*token_id_external))) + } +} + +fn hash(token_id: &str) -> GenericArray { + let mut hasher = Keccak256::new(); + hasher.update(token_id); + hasher.finalize() +} + +pub fn to_external_token_id( + storage: &mut dyn Storage, + nft_chain: u16, + nft_address: &[u8; 32], + token_id_internal: String, +) -> StdResult<[u8; 32]> { + if nft_chain == CHAIN_ID { + let hash = hash(&token_id_internal); + token_id_hashes(storage, nft_chain, *nft_address).save(&hash, &token_id_internal)?; + Ok(hash.as_slice().get_const_bytes(0)) + } else { + Ok(Uint256::from_str(&token_id_internal)?.to_be_bytes()) + } +} diff --git a/wormhole/token-bridge/.cargo/config b/wormhole/token-bridge/.cargo/config new file mode 100644 index 00000000..2d5cce4e --- /dev/null +++ b/wormhole/token-bridge/.cargo/config @@ -0,0 +1,5 @@ +[alias] +wasm = "build --release --target wasm32-unknown-unknown" +wasm-debug = "build --target wasm32-unknown-unknown" +unit-test = "test --lib --features backtraces" +integration-test = "test --test integration" \ No newline at end of file diff --git a/wormhole/token-bridge/Cargo.toml b/wormhole/token-bridge/Cargo.toml new file mode 100644 index 00000000..3dea0a0e --- /dev/null +++ b/wormhole/token-bridge/Cargo.toml @@ -0,0 +1,35 @@ +[package] +name = "token-bridge-terra" +version = "0.1.0" +authors = ["Yuriy Savchenko "] +edition = "2018" +description = "Wormhole token bridge" + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +backtraces = ["cosmwasm-std/backtraces"] +# use library feature to disable all init/handle/query exports +library = [] + +[dependencies] +cosmwasm-std = { version = "0.16.0" } +cosmwasm-storage = { version = "0.16.0" } +schemars = "0.8.1" +serde = { version = "1.0.103", default-features = false, features = ["derive"] } +cw20 = "0.8.0" +cw20-base = { version = "0.8.0", features = ["library"] } +cw20-wrapped = { path = "../cw20-wrapped", features = ["library"] } +terraswap = "2.4.0" +wormhole-bridge-terra = { path = "../wormhole", features = ["library"] } +thiserror = { version = "1.0.20" } +k256 = { version = "0.9.4", default-features = false, features = ["ecdsa"] } +sha3 = { version = "0.9.1", default-features = false } +generic-array = { version = "0.14.4" } +hex = "0.4.2" +lazy_static = "1.4.0" +bigint = "4" + +[dev-dependencies] +serde_json = "1.0" diff --git a/wormhole/token-bridge/_tests/integration.rs b/wormhole/token-bridge/_tests/integration.rs new file mode 100644 index 00000000..88d0359b --- /dev/null +++ b/wormhole/token-bridge/_tests/integration.rs @@ -0,0 +1,114 @@ +static WASM: &[u8] = include_bytes!("../../../target/wasm32-unknown-unknown/release/wormhole.wasm"); + +use cosmwasm_std::{ + from_slice, + Coin, + Env, + HumanAddr, + InitResponse, +}; +use cosmwasm_storage::to_length_prefixed; +use cosmwasm_vm::{ + testing::{ + init, + mock_env, + mock_instance, + MockApi, + MockQuerier, + MockStorage, + }, + Api, + Instance, + Storage, +}; + +use wormhole::{ + msg::InitMsg, + state::{ + ConfigInfo, + GuardianAddress, + GuardianSetInfo, + CONFIG_KEY, + }, +}; + +use hex; + +enum TestAddress { + INITIALIZER, +} + +impl TestAddress { + fn value(&self) -> HumanAddr { + match self { + TestAddress::INITIALIZER => HumanAddr::from("initializer"), + } + } +} + +fn mock_env_height(signer: &HumanAddr, height: u64, time: u64) -> Env { + let mut env = mock_env(signer, &[]); + env.block.height = height; + env.block.time = time; + env +} + +fn get_config_info(storage: &S) -> ConfigInfo { + let key = to_length_prefixed(CONFIG_KEY); + let data = storage + .get(&key) + .0 + .expect("error getting data") + .expect("data should exist"); + from_slice(&data).expect("invalid data") +} + +fn do_init( + height: u64, + guardians: &Vec, +) -> Instance { + let mut deps = mock_instance(WASM, &[]); + let init_msg = InitMsg { + initial_guardian_set: GuardianSetInfo { + addresses: guardians.clone(), + expiration_time: 100, + }, + guardian_set_expirity: 50, + wrapped_asset_code_id: 999, + }; + let env = mock_env_height(&TestAddress::INITIALIZER.value(), height, 0); + let owner = deps + .api + .canonical_address(&TestAddress::INITIALIZER.value()) + .0 + .unwrap(); + let res: InitResponse = init(&mut deps, env, init_msg).unwrap(); + assert_eq!(0, res.messages.len()); + + // query the store directly + deps.with_storage(|storage| { + assert_eq!( + get_config_info(storage), + ConfigInfo { + guardian_set_index: 0, + guardian_set_expirity: 50, + wrapped_asset_code_id: 999, + owner, + fee: Coin::new(10000, "uluna"), + } + ); + Ok(()) + }) + .unwrap(); + deps +} + +#[test] +fn init_works() { + let guardians = vec![GuardianAddress::from(GuardianAddress { + bytes: hex::decode("beFA429d57cD18b7F8A4d91A2da9AB4AF05d0FBe") + .expect("Decoding failed") + .into(), + })]; + let _deps = do_init(111, &guardians); +} diff --git a/wormhole/token-bridge/src/contract.rs b/wormhole/token-bridge/src/contract.rs new file mode 100644 index 00000000..8b7a087c --- /dev/null +++ b/wormhole/token-bridge/src/contract.rs @@ -0,0 +1,1318 @@ +use cw20::{BalanceResponse, TokenInfoResponse}; +use cw20_base::msg::{ExecuteMsg as TokenMsg, QueryMsg as TokenQuery}; +use cw20_wrapped::msg::{ + ExecuteMsg as WrappedMsg, InitHook, InstantiateMsg as WrappedInit, QueryMsg as WrappedQuery, + WrappedAssetInfoResponse, +}; +use sha3::{Digest, Keccak256}; +use std::cmp::{max, min}; +use std::str::FromStr; +use terraswap::asset::{Asset, AssetInfo}; + +use wormhole::byte_utils::{ + extend_address_to_32, extend_string_to_32, get_string_from_32, ByteUtils, +}; +use wormhole::error::ContractError; +use wormhole::msg::{ExecuteMsg as WormholeExecuteMsg, QueryMsg as WormholeQueryMsg}; +use wormhole::state::{vaa_archive_add, vaa_archive_check, GovernancePacket, ParsedVAA}; + +#[cfg(not(feature = "library"))] +use cosmwasm_std::entry_point; +use cosmwasm_std::{ + coin, to_binary, BankMsg, Binary, CanonicalAddr, Coin, CosmosMsg, Deps, DepsMut, Empty, Env, + MessageInfo, QueryRequest, Reply, Response, StdError, StdResult, SubMsg, Uint128, WasmMsg, + WasmQuery, +}; + +use crate::msg::{ + ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg, TransferInfoResponse, WrappedRegistryResponse, +}; +use crate::state::{ + bridge_contracts, bridge_contracts_read, bridge_deposit, config, config_read, receive_native, + send_native, wrapped_asset, wrapped_asset_address, wrapped_asset_address_read, + wrapped_asset_read, wrapped_asset_seq, wrapped_asset_seq_read, wrapped_transfer_tmp, Action, + AssetMeta, ConfigInfo, RegisterChain, TokenBridgeMessage, TransferInfo, TransferState, + TransferWithPayloadInfo, UpgradeContract, +}; + +type HumanAddr = String; + +// Chain ID of Terra +const CHAIN_ID: u16 = 3; + +const WRAPPED_ASSET_UPDATING: &str = "updating"; + +pub enum TransferType { + WithoutPayload, + WithPayload { payload: A }, +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn migrate(_deps: DepsMut, _env: Env, _msg: MigrateMsg) -> StdResult { + Ok(Response::new()) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn instantiate( + deps: DepsMut, + _env: Env, + _info: MessageInfo, + msg: InstantiateMsg, +) -> StdResult { + // Save general wormhole info + let state = ConfigInfo { + gov_chain: msg.gov_chain, + gov_address: msg.gov_address.into(), + wormhole_contract: msg.wormhole_contract, + wrapped_asset_code_id: msg.wrapped_asset_code_id, + }; + config(deps.storage).save(&state)?; + + Ok(Response::default()) +} + +// When CW20 transfers complete, we need to verify the actual amount that is being transferred out +// of the bridge. This is to handle fee tokens where the amount expected to be transferred may be +// less due to burns, fees, etc. +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn reply(deps: DepsMut, env: Env, _msg: Reply) -> StdResult { + let cfg = config_read(deps.storage).load()?; + + let state = wrapped_transfer_tmp(deps.storage).load()?; + // NOTE: Reentrancy protection. See note in `handle_initiate_transfer_token` + // for why this is necessary. + wrapped_transfer_tmp(deps.storage).remove(); + + let token_bridge_message = TokenBridgeMessage::deserialize(&state.message)?; + + let (mut transfer_info, transfer_type) = match token_bridge_message.action { + Action::TRANSFER => { + let info = TransferInfo::deserialize(&token_bridge_message.payload)?; + Ok((info, TransferType::WithoutPayload)) + } + Action::TRANSFER_WITH_PAYLOAD => { + let info = TransferWithPayloadInfo::deserialize(&token_bridge_message.payload)?; + Ok(( + info.transfer_info, + TransferType::WithPayload { + payload: info.payload, + }, + )) + } + _ => Err(StdError::generic_err("Unreachable")), + }?; + + // Fetch CW20 Balance post-transfer. + let new_balance: BalanceResponse = + deps.querier.query(&QueryRequest::Wasm(WasmQuery::Smart { + contract_addr: state.token_address.clone(), + msg: to_binary(&TokenQuery::Balance { + address: env.contract.address.to_string(), + })?, + }))?; + + // Actual amount should be the difference in balance of the CW20 account in question to account + // for fee tokens. + let multiplier = Uint128::from_str(&state.multiplier)?; + let real_amount = new_balance.balance - Uint128::from_str(&state.previous_balance)?; + let real_amount = real_amount / multiplier; + + // If the fee is too large the user would receive nothing. + if transfer_info.fee.1 > real_amount.u128() { + return Err(StdError::generic_err("fee greater than sent amount")); + } + + // Update Wormhole message to correct amount. + transfer_info.amount.1 = real_amount.u128(); + + let token_bridge_message = match transfer_type { + TransferType::WithoutPayload => TokenBridgeMessage { + action: Action::TRANSFER, + payload: transfer_info.serialize(), + }, + TransferType::WithPayload { payload } => TokenBridgeMessage { + action: Action::TRANSFER_WITH_PAYLOAD, + payload: TransferWithPayloadInfo { + transfer_info, + payload, + } + .serialize(), + }, + }; + + // Post Wormhole Message + let message = CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: cfg.wormhole_contract, + funds: vec![], + msg: to_binary(&WormholeExecuteMsg::PostMessage { + message: Binary::from(token_bridge_message.serialize()), + nonce: state.nonce, + })?, + }); + + send_native(deps.storage, &state.token_canonical, real_amount)?; + Ok(Response::default() + .add_message(message) + .add_attribute("action", "reply_handler")) +} + +pub fn coins_after_tax(deps: DepsMut, coins: Vec) -> StdResult> { + let mut res = vec![]; + for coin in coins { + let asset = Asset { + amount: coin.amount, + info: AssetInfo::NativeToken { + denom: coin.denom.clone(), + }, + }; + res.push(asset.deduct_tax(&deps.querier)?); + } + Ok(res) +} + +fn parse_vaa(deps: Deps, block_time: u64, data: &Binary) -> StdResult { + let cfg = config_read(deps.storage).load()?; + let vaa: ParsedVAA = deps.querier.query(&QueryRequest::Wasm(WasmQuery::Smart { + contract_addr: cfg.wormhole_contract, + msg: to_binary(&WormholeQueryMsg::VerifyVAA { + vaa: data.clone(), + block_time, + })?, + }))?; + Ok(vaa) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn execute(deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg) -> StdResult { + match msg { + ExecuteMsg::RegisterAssetHook { asset_id } => { + handle_register_asset(deps, env, info, &asset_id) + } + ExecuteMsg::InitiateTransfer { + asset, + recipient_chain, + recipient, + fee, + nonce, + } => handle_initiate_transfer( + deps, + env, + info, + asset, + recipient_chain, + recipient.into(), + fee, + TransferType::WithoutPayload, + nonce, + ), + ExecuteMsg::InitiateTransferWithPayload { + asset, + recipient_chain, + recipient, + fee, + payload, + nonce, + } => handle_initiate_transfer( + deps, + env, + info, + asset, + recipient_chain, + recipient.into(), + fee, + TransferType::WithPayload { + payload: payload.into(), + }, + nonce, + ), + ExecuteMsg::DepositTokens {} => deposit_tokens(deps, env, info), + ExecuteMsg::WithdrawTokens { asset } => withdraw_tokens(deps, env, info, asset), + ExecuteMsg::SubmitVaa { data } => submit_vaa(deps, env, info, &data), + ExecuteMsg::CreateAssetMeta { asset_info, nonce } => { + handle_create_asset_meta(deps, env, info, asset_info, nonce) + } + ExecuteMsg::CompleteTransferWithPayload { data, relayer } => { + handle_complete_transfer_with_payload(deps, env, info, &data, &relayer) + } + } +} + +fn deposit_tokens(deps: DepsMut, _env: Env, info: MessageInfo) -> StdResult { + for coin in info.funds { + let deposit_key = format!("{}:{}", info.sender, coin.denom); + bridge_deposit(deps.storage).update( + deposit_key.as_bytes(), + |amount: Option| -> StdResult { + Ok(amount.unwrap_or(Uint128::new(0)) + coin.amount) + }, + )?; + } + + Ok(Response::new().add_attribute("action", "deposit_tokens")) +} + +fn withdraw_tokens( + deps: DepsMut, + _env: Env, + info: MessageInfo, + data: AssetInfo, +) -> StdResult { + let mut messages: Vec = vec![]; + if let AssetInfo::NativeToken { denom } = data { + let deposit_key = format!("{}:{}", info.sender, denom); + let mut deposited_amount: u128 = 0; + bridge_deposit(deps.storage).update( + deposit_key.as_bytes(), + |current: Option| match current { + Some(v) => { + deposited_amount = v.u128(); + Ok(Uint128::new(0)) + } + None => Err(StdError::generic_err("no deposit found to withdraw")), + }, + )?; + messages.push(CosmosMsg::Bank(BankMsg::Send { + to_address: info.sender.to_string(), + amount: coins_after_tax(deps, vec![coin(deposited_amount, &denom)])?, + })); + } + + Ok(Response::new() + .add_messages(messages) + .add_attribute("action", "withdraw_tokens")) +} + +/// Handle wrapped asset registration messages +fn handle_register_asset( + deps: DepsMut, + _env: Env, + info: MessageInfo, + asset_id: &[u8], +) -> StdResult { + let mut bucket = wrapped_asset(deps.storage); + let result = bucket.load(asset_id); + let result = result.map_err(|_| ContractError::RegistrationForbidden.std())?; + if result != WRAPPED_ASSET_UPDATING { + return ContractError::AssetAlreadyRegistered.std_err(); + } + + bucket.save(asset_id, &info.sender.to_string())?; + + let contract_address: CanonicalAddr = deps.api.addr_canonicalize(info.sender.as_str())?; + wrapped_asset_address(deps.storage).save(contract_address.as_slice(), &asset_id.to_vec())?; + + Ok(Response::new() + .add_attribute("action", "register_asset") + .add_attribute("asset_id", format!("{:?}", asset_id)) + .add_attribute("contract_addr", info.sender)) +} + +fn handle_attest_meta( + deps: DepsMut, + env: Env, + emitter_chain: u16, + emitter_address: Vec, + sequence: u64, + data: &Vec, +) -> StdResult { + let meta = AssetMeta::deserialize(data)?; + + let expected_contract = + bridge_contracts_read(deps.storage).load(&emitter_chain.to_be_bytes())?; + + // must be sent by a registered token bridge contract + if expected_contract != emitter_address { + return Err(StdError::generic_err("invalid emitter")); + } + + if CHAIN_ID == meta.token_chain { + return Err(StdError::generic_err( + "this asset is native to this chain and should not be attested", + )); + } + + let cfg = config_read(deps.storage).load()?; + let asset_id = build_asset_id(meta.token_chain, &meta.token_address); + + // If a CW20 wrapped already exists and this message has a newer sequence ID + // we allow updating the metadata. If not, we create a brand new token. + let message = if let Ok(contract) = wrapped_asset_read(deps.storage).load(&asset_id) { + // Prevent anyone from re-attesting with old VAAs. + if sequence <= wrapped_asset_seq_read(deps.storage).load(&asset_id)? { + return Err(StdError::generic_err( + "this asset has already been attested", + )); + } + CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: contract, + msg: to_binary(&WrappedMsg::UpdateMetadata { + name: get_string_from_32(&meta.name), + symbol: get_string_from_32(&meta.symbol), + })?, + funds: vec![], + }) + } else { + wrapped_asset(deps.storage).save(&asset_id, &HumanAddr::from(WRAPPED_ASSET_UPDATING))?; + CosmosMsg::Wasm(WasmMsg::Instantiate { + admin: Some(env.contract.address.clone().into_string()), + code_id: cfg.wrapped_asset_code_id, + msg: to_binary(&WrappedInit { + name: get_string_from_32(&meta.name), + symbol: get_string_from_32(&meta.symbol), + asset_chain: meta.token_chain, + asset_address: meta.token_address.to_vec().into(), + decimals: min(meta.decimals, 8u8), + mint: None, + init_hook: Some(InitHook { + contract_addr: env.contract.address.to_string(), + msg: to_binary(&ExecuteMsg::RegisterAssetHook { + asset_id: asset_id.to_vec().into(), + })?, + }), + })?, + funds: vec![], + label: String::new(), + }) + }; + wrapped_asset_seq(deps.storage).save(&asset_id, &sequence)?; + Ok(Response::new().add_message(message)) +} + +fn handle_create_asset_meta( + deps: DepsMut, + env: Env, + info: MessageInfo, + asset_info: AssetInfo, + nonce: u32, +) -> StdResult { + match asset_info { + AssetInfo::Token { contract_addr } => { + handle_create_asset_meta_token(deps, env, info, contract_addr, nonce) + } + AssetInfo::NativeToken { ref denom } => { + handle_create_asset_meta_native_token(deps, env, info, denom.clone(), nonce) + } + } +} + +fn handle_create_asset_meta_token( + deps: DepsMut, + env: Env, + info: MessageInfo, + asset_address: HumanAddr, + nonce: u32, +) -> StdResult { + let cfg = config_read(deps.storage).load()?; + + let request = QueryRequest::Wasm(WasmQuery::Smart { + contract_addr: asset_address.clone(), + msg: to_binary(&TokenQuery::TokenInfo {})?, + }); + + let asset_canonical = deps.api.addr_canonicalize(&asset_address)?; + let token_info: TokenInfoResponse = deps.querier.query(&request)?; + + let meta: AssetMeta = AssetMeta { + token_chain: CHAIN_ID, + token_address: extend_address_to_32(&asset_canonical), + decimals: token_info.decimals, + symbol: extend_string_to_32(&token_info.symbol), + name: extend_string_to_32(&token_info.name), + }; + + let token_bridge_message = TokenBridgeMessage { + action: Action::ATTEST_META, + payload: meta.serialize().to_vec(), + }; + + Ok(Response::new() + .add_message(CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: cfg.wormhole_contract, + msg: to_binary(&WormholeExecuteMsg::PostMessage { + message: Binary::from(token_bridge_message.serialize()), + nonce, + })?, + // forward coins sent to this message + funds: coins_after_tax(deps, info.funds)?, + })) + .add_attribute("meta.token_chain", CHAIN_ID.to_string()) + .add_attribute("meta.token", asset_address) + .add_attribute("meta.nonce", nonce.to_string()) + .add_attribute("meta.block_time", env.block.time.seconds().to_string())) +} + +fn handle_create_asset_meta_native_token( + deps: DepsMut, + env: Env, + info: MessageInfo, + denom: String, + nonce: u32, +) -> StdResult { + let cfg = config_read(deps.storage).load()?; + let mut asset_id = extend_address_to_32(&build_native_id(&denom).into()); + asset_id[0] = 1; + let symbol = format_native_denom_symbol(&denom); + let meta: AssetMeta = AssetMeta { + token_chain: CHAIN_ID, + token_address: asset_id.clone(), + decimals: 6, + symbol: extend_string_to_32(&symbol), + name: extend_string_to_32(&symbol), + }; + let token_bridge_message = TokenBridgeMessage { + action: Action::ATTEST_META, + payload: meta.serialize().to_vec(), + }; + Ok(Response::new() + .add_message(CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: cfg.wormhole_contract, + msg: to_binary(&WormholeExecuteMsg::PostMessage { + message: Binary::from(token_bridge_message.serialize()), + nonce, + })?, + // forward coins sent to this message + funds: coins_after_tax(deps, info.funds)?, + })) + .add_attribute("meta.token_chain", CHAIN_ID.to_string()) + .add_attribute("meta.symbol", symbol) + .add_attribute("meta.asset_id", hex::encode(asset_id)) + .add_attribute("meta.nonce", nonce.to_string()) + .add_attribute("meta.block_time", env.block.time.seconds().to_string())) +} + +fn handle_complete_transfer_with_payload( + deps: DepsMut, + env: Env, + info: MessageInfo, + data: &Binary, + relayer_address: &HumanAddr, +) -> StdResult { + let state = config_read(deps.storage).load()?; + + let vaa = parse_vaa(deps.as_ref(), env.block.time.seconds(), data)?; + let data = vaa.payload; + + if vaa_archive_check(deps.storage, vaa.hash.as_slice()) { + return ContractError::VaaAlreadyExecuted.std_err(); + } + vaa_archive_add(deps.storage, vaa.hash.as_slice())?; + + // check if vaa is from governance + if is_governance_emitter(&state, vaa.emitter_chain, &vaa.emitter_address) { + return ContractError::InvalidVAAAction.std_err(); + } + + let message = TokenBridgeMessage::deserialize(&data)?; + + match message.action { + Action::TRANSFER_WITH_PAYLOAD => handle_complete_transfer( + deps, + env, + info, + vaa.emitter_chain, + vaa.emitter_address, + TransferType::WithPayload { payload: () }, + &message.payload, + relayer_address, + ), + _ => ContractError::InvalidVAAAction.std_err(), + } +} + +fn submit_vaa(deps: DepsMut, env: Env, info: MessageInfo, data: &Binary) -> StdResult { + let state = config_read(deps.storage).load()?; + + let vaa = parse_vaa(deps.as_ref(), env.block.time.seconds(), data)?; + let data = vaa.payload; + + if vaa_archive_check(deps.storage, vaa.hash.as_slice()) { + return ContractError::VaaAlreadyExecuted.std_err(); + } + vaa_archive_add(deps.storage, vaa.hash.as_slice())?; + + // check if vaa is from governance + if is_governance_emitter(&state, vaa.emitter_chain, &vaa.emitter_address) { + return handle_governance_payload(deps, env, &data); + } + + let message = TokenBridgeMessage::deserialize(&data)?; + + match message.action { + Action::TRANSFER => { + let sender = info.sender.to_string(); + handle_complete_transfer( + deps, + env, + info, + vaa.emitter_chain, + vaa.emitter_address, + TransferType::WithoutPayload, + &message.payload, + &sender, + ) + } + Action::ATTEST_META => handle_attest_meta( + deps, + env, + vaa.emitter_chain, + vaa.emitter_address, + vaa.sequence, + &message.payload, + ), + _ => ContractError::InvalidVAAAction.std_err(), + } +} + +fn handle_governance_payload(deps: DepsMut, env: Env, data: &[u8]) -> StdResult { + let gov_packet = GovernancePacket::deserialize(data)?; + let module = get_string_from_32(&gov_packet.module); + + if module != "TokenBridge" { + return Err(StdError::generic_err("this is not a valid module")); + } + + if gov_packet.chain != 0 && gov_packet.chain != CHAIN_ID { + return Err(StdError::generic_err( + "the governance VAA is for another chain", + )); + } + + match gov_packet.action { + 1u8 => handle_register_chain(deps, env, &gov_packet.payload), + 2u8 => handle_upgrade_contract(deps, env, &gov_packet.payload), + _ => ContractError::InvalidVAAAction.std_err(), + } +} + +fn handle_upgrade_contract(_deps: DepsMut, env: Env, data: &Vec) -> StdResult { + let UpgradeContract { new_contract } = UpgradeContract::deserialize(data)?; + + Ok(Response::new() + .add_message(CosmosMsg::Wasm(WasmMsg::Migrate { + contract_addr: env.contract.address.to_string(), + new_code_id: new_contract, + msg: to_binary(&MigrateMsg {})?, + })) + .add_attribute("action", "contract_upgrade")) +} + +fn handle_register_chain(deps: DepsMut, _env: Env, data: &Vec) -> StdResult { + let RegisterChain { + chain_id, + chain_address, + } = RegisterChain::deserialize(data)?; + + let existing = bridge_contracts_read(deps.storage).load(&chain_id.to_be_bytes()); + if existing.is_ok() { + return Err(StdError::generic_err( + "bridge contract already exists for this chain", + )); + } + + let mut bucket = bridge_contracts(deps.storage); + bucket.save(&chain_id.to_be_bytes(), &chain_address)?; + + Ok(Response::new() + .add_attribute("chain_id", chain_id.to_string()) + .add_attribute("chain_address", hex::encode(chain_address))) +} + +#[allow(clippy::too_many_arguments)] +fn handle_complete_transfer( + deps: DepsMut, + env: Env, + info: MessageInfo, + emitter_chain: u16, + emitter_address: Vec, + transfer_type: TransferType<()>, + data: &[u8], + relayer_address: &HumanAddr, +) -> StdResult { + let transfer_info = TransferInfo::deserialize(data)?; + match transfer_info.token_address[0] { + 1 => handle_complete_transfer_token_native( + deps, + env, + info, + emitter_chain, + emitter_address, + transfer_type, + data, + relayer_address, + ), + _ => handle_complete_transfer_token( + deps, + env, + info, + emitter_chain, + emitter_address, + transfer_type, + data, + relayer_address, + ), + } +} + +#[allow(clippy::too_many_arguments)] +fn handle_complete_transfer_token( + deps: DepsMut, + _env: Env, + info: MessageInfo, + emitter_chain: u16, + emitter_address: Vec, + transfer_type: TransferType<()>, + data: &[u8], + relayer_address: &HumanAddr, +) -> StdResult { + let transfer_info = match transfer_type { + TransferType::WithoutPayload => TransferInfo::deserialize(data)?, + TransferType::WithPayload { payload: _ } => { + TransferWithPayloadInfo::deserialize(data)?.transfer_info + } + }; + + let expected_contract = + bridge_contracts_read(deps.storage).load(&emitter_chain.to_be_bytes())?; + + // must be sent by a registered token bridge contract + if expected_contract != emitter_address { + return Err(StdError::generic_err("invalid emitter")); + } + + if transfer_info.recipient_chain != CHAIN_ID { + return Err(StdError::generic_err( + "this transfer is not directed at this chain", + )); + } + + let token_chain = transfer_info.token_chain; + let target_address = (&transfer_info.recipient.as_slice()).get_address(0); + let recipient = deps.api.addr_humanize(&target_address)?; + + if let TransferType::WithPayload { payload: _ } = transfer_type { + if recipient != info.sender { + return Err(StdError::generic_err( + "transfers with payload can only be redeemed by the recipient", + )); + } + }; + + let (not_supported_amount, mut amount) = transfer_info.amount; + let (not_supported_fee, mut fee) = transfer_info.fee; + + amount = amount.checked_sub(fee).unwrap(); + + // Check high 128 bit of amount value to be empty + if not_supported_amount != 0 || not_supported_fee != 0 { + return ContractError::AmountTooHigh.std_err(); + } + + if token_chain != CHAIN_ID { + let asset_address = transfer_info.token_address; + let asset_id = build_asset_id(token_chain, &asset_address); + + // Check if this asset is already deployed + let contract_addr = wrapped_asset_read(deps.storage).load(&asset_id).ok(); + + if let Some(contract_addr) = contract_addr { + // Asset already deployed, just mint + + let mut messages = vec![CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: contract_addr.clone(), + msg: to_binary(&WrappedMsg::Mint { + recipient: recipient.to_string(), + amount: Uint128::from(amount), + })?, + funds: vec![], + })]; + if fee != 0 { + messages.push(CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: contract_addr.clone(), + msg: to_binary(&WrappedMsg::Mint { + recipient: relayer_address.to_string(), + amount: Uint128::from(fee), + })?, + funds: vec![], + })) + } + + Ok(Response::new() + .add_messages(messages) + .add_attribute("action", "complete_transfer_wrapped") + .add_attribute("contract", contract_addr) + .add_attribute("recipient", recipient) + .add_attribute("amount", amount.to_string()) + .add_attribute("relayer", relayer_address) + .add_attribute("fee", fee.to_string())) + } else { + Err(StdError::generic_err("Wrapped asset not deployed. To deploy, invoke CreateWrapped with the associated AssetMeta")) + } + } else { + let token_address = transfer_info.token_address.as_slice().get_address(0); + + let contract_addr = deps.api.addr_humanize(&token_address)?; + + // note -- here the amount is the amount the recipient will receive; + // amount + fee is the total sent + receive_native(deps.storage, &token_address, Uint128::new(amount + fee))?; + + // undo normalization to 8 decimals + let token_info: TokenInfoResponse = + deps.querier.query(&QueryRequest::Wasm(WasmQuery::Smart { + contract_addr: contract_addr.to_string(), + msg: to_binary(&TokenQuery::TokenInfo {})?, + }))?; + + let decimals = token_info.decimals; + let multiplier = 10u128.pow((max(decimals, 8u8) - 8u8) as u32); + amount = amount.checked_mul(multiplier).unwrap(); + fee = fee.checked_mul(multiplier).unwrap(); + + let mut messages = vec![CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: contract_addr.to_string(), + msg: to_binary(&TokenMsg::Transfer { + recipient: recipient.to_string(), + amount: Uint128::from(amount), + })?, + funds: vec![], + })]; + + if fee != 0 { + messages.push(CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: contract_addr.to_string(), + msg: to_binary(&TokenMsg::Transfer { + recipient: relayer_address.to_string(), + amount: Uint128::from(fee), + })?, + funds: vec![], + })) + } + + Ok(Response::new() + .add_messages(messages) + .add_attribute("action", "complete_transfer_native") + .add_attribute("recipient", recipient) + .add_attribute("contract", contract_addr) + .add_attribute("amount", amount.to_string()) + .add_attribute("relayer", relayer_address) + .add_attribute("fee", fee.to_string())) + } +} + +#[allow(clippy::too_many_arguments)] +fn handle_complete_transfer_token_native( + mut deps: DepsMut, + _env: Env, + info: MessageInfo, + emitter_chain: u16, + emitter_address: Vec, + transfer_type: TransferType<()>, + data: &[u8], + relayer_address: &HumanAddr, +) -> StdResult { + let transfer_info = match transfer_type { + TransferType::WithoutPayload => TransferInfo::deserialize(data)?, + TransferType::WithPayload { payload: () } => { + TransferWithPayloadInfo::deserialize(data)?.transfer_info + } + }; + + let expected_contract = + bridge_contracts_read(deps.storage).load(&emitter_chain.to_be_bytes())?; + + // must be sent by a registered token bridge contract + if expected_contract != emitter_address { + return Err(StdError::generic_err("invalid emitter")); + } + + if transfer_info.recipient_chain != CHAIN_ID { + return Err(StdError::generic_err( + "this transfer is not directed at this chain", + )); + } + + let target_address = (&transfer_info.recipient.as_slice()).get_address(0); + let recipient = deps.api.addr_humanize(&target_address)?; + + if let TransferType::WithPayload { payload: _ } = transfer_type { + if recipient != info.sender { + return Err(StdError::generic_err( + "transfers with payload can only be redeemed by the recipient", + )); + } + }; + + let (not_supported_amount, mut amount) = transfer_info.amount; + let (not_supported_fee, fee) = transfer_info.fee; + + amount = amount.checked_sub(fee).unwrap(); + + // Check high 128 bit of amount value to be empty + if not_supported_amount != 0 || not_supported_fee != 0 { + return ContractError::AmountTooHigh.std_err(); + } + + // Wipe the native byte marker and extract the serialized denom. + let mut token_address = transfer_info.token_address; + let token_address = token_address.as_mut_slice(); + token_address[0] = 0; + + let mut denom = token_address.to_vec(); + denom.retain(|&c| c != 0); + let denom = String::from_utf8(denom).unwrap(); + + // note -- here the amount is the amount the recipient will receive; + // amount + fee is the total sent + let token_address = (&*token_address).get_address(0); + receive_native(deps.storage, &token_address, Uint128::new(amount + fee))?; + + let mut messages = vec![CosmosMsg::Bank(BankMsg::Send { + to_address: recipient.to_string(), + amount: coins_after_tax(deps.branch(), vec![coin(amount, &denom)])?, + })]; + + if fee != 0 { + messages.push(CosmosMsg::Bank(BankMsg::Send { + to_address: relayer_address.to_string(), + amount: coins_after_tax(deps, vec![coin(fee, &denom)])?, + })); + } + + Ok(Response::new() + .add_messages(messages) + .add_attribute("action", "complete_transfer_terra_native") + .add_attribute("recipient", recipient) + .add_attribute("denom", denom) + .add_attribute("amount", amount.to_string()) + .add_attribute("relayer", relayer_address) + .add_attribute("fee", fee.to_string())) +} + +#[allow(clippy::too_many_arguments)] +fn handle_initiate_transfer( + deps: DepsMut, + env: Env, + info: MessageInfo, + asset: Asset, + recipient_chain: u16, + recipient: Vec, + fee: Uint128, + transfer_type: TransferType>, + nonce: u32, +) -> StdResult { + match asset.info { + AssetInfo::Token { contract_addr } => handle_initiate_transfer_token( + deps, + env, + info, + contract_addr, + asset.amount, + recipient_chain, + recipient, + fee, + transfer_type, + nonce, + ), + AssetInfo::NativeToken { ref denom } => handle_initiate_transfer_native_token( + deps, + env, + info, + denom.clone(), + asset.amount, + recipient_chain, + recipient, + fee, + transfer_type, + nonce, + ), + } +} + +#[allow(clippy::too_many_arguments)] +fn handle_initiate_transfer_token( + mut deps: DepsMut, + env: Env, + info: MessageInfo, + asset: HumanAddr, + mut amount: Uint128, + recipient_chain: u16, + recipient: Vec, + mut fee: Uint128, + transfer_type: TransferType>, + nonce: u32, +) -> StdResult { + if recipient_chain == CHAIN_ID { + return ContractError::SameSourceAndTarget.std_err(); + } + if amount.is_zero() { + return ContractError::AmountTooLow.std_err(); + } + + let asset_chain: u16; + let asset_address: Vec; + + let cfg: ConfigInfo = config_read(deps.storage).load()?; + let asset_canonical: CanonicalAddr = deps.api.addr_canonicalize(&asset)?; + + let mut messages: Vec = vec![]; + let mut submessages: Vec = vec![]; + + match wrapped_asset_address_read(deps.storage).load(asset_canonical.as_slice()) { + Ok(_) => { + // If the fee is too large the user will receive nothing. + if fee > amount { + return Err(StdError::generic_err("fee greater than sent amount")); + } + + // This is a deployed wrapped asset, burn it + messages.push(CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: asset.clone(), + msg: to_binary(&WrappedMsg::Burn { + account: info.sender.to_string(), + amount, + })?, + funds: vec![], + })); + let request = QueryRequest::::Wasm(WasmQuery::Smart { + contract_addr: asset, + msg: to_binary(&WrappedQuery::WrappedAssetInfo {})?, + }); + let wrapped_token_info: WrappedAssetInfoResponse = + deps.querier.custom_query(&request)?; + asset_chain = wrapped_token_info.asset_chain; + asset_address = wrapped_token_info.asset_address.into(); + + let transfer_info = TransferInfo { + token_chain: asset_chain, + token_address: asset_address.clone(), + amount: (0, amount.u128()), + recipient_chain, + recipient: recipient.clone(), + fee: (0, fee.u128()), + }; + + let token_bridge_message: TokenBridgeMessage = match transfer_type { + TransferType::WithoutPayload => TokenBridgeMessage { + action: Action::TRANSFER, + payload: transfer_info.serialize(), + }, + TransferType::WithPayload { payload } => TokenBridgeMessage { + action: Action::TRANSFER_WITH_PAYLOAD, + payload: TransferWithPayloadInfo { + transfer_info, + payload, + } + .serialize(), + }, + }; + + messages.push(CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: cfg.wormhole_contract, + msg: to_binary(&WormholeExecuteMsg::PostMessage { + message: Binary::from(token_bridge_message.serialize()), + nonce, + })?, + // forward coins sent to this message + funds: coins_after_tax(deps.branch(), info.funds.clone())?, + })); + } + Err(_) => { + // normalize amount to 8 decimals when it sent over the wormhole + let token_info: TokenInfoResponse = + deps.querier.query(&QueryRequest::Wasm(WasmQuery::Smart { + contract_addr: asset.clone(), + msg: to_binary(&TokenQuery::TokenInfo {})?, + }))?; + + let decimals = token_info.decimals; + let multiplier = 10u128.pow((max(decimals, 8u8) - 8u8) as u32); + + // chop off dust + amount = Uint128::new( + amount + .u128() + .checked_sub(amount.u128().checked_rem(multiplier).unwrap()) + .unwrap(), + ); + + fee = Uint128::new( + fee.u128() + .checked_sub(fee.u128().checked_rem(multiplier).unwrap()) + .unwrap(), + ); + + // This is a regular asset, transfer its balance + submessages.push(SubMsg::reply_on_success( + CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: asset.clone(), + msg: to_binary(&TokenMsg::TransferFrom { + owner: info.sender.to_string(), + recipient: env.contract.address.to_string(), + amount, + })?, + funds: vec![], + }), + 1, + )); + + asset_address = extend_address_to_32(&asset_canonical); + asset_chain = CHAIN_ID; + + // convert to normalized amounts before recording & posting vaa + amount = Uint128::new(amount.u128().checked_div(multiplier).unwrap()); + fee = Uint128::new(fee.u128().checked_div(multiplier).unwrap()); + + let transfer_info = TransferInfo { + token_chain: asset_chain, + token_address: asset_address.clone(), + amount: (0, amount.u128()), + recipient_chain, + recipient: recipient.clone(), + fee: (0, fee.u128()), + }; + + // Fetch current CW20 Balance pre-transfer. + let balance: BalanceResponse = + deps.querier.query(&QueryRequest::Wasm(WasmQuery::Smart { + contract_addr: asset.to_string(), + msg: to_binary(&TokenQuery::Balance { + address: env.contract.address.to_string(), + })?, + }))?; + + // NOTE: Reentrancy protection. It is crucial that there's no + // ongoing transfer in progress here, otherwise we would override + // its state. This could happen if the asset's TransferFrom handler + // sends us an InitiateTransfer message, which would be executed + // before the reply handler due the the depth-first semantics of + // message execution. A simple protection mechanism is to require + // that there's no execution in progress. The reply handler takes + // care of clearing out this temporary storage when done. + assert!(wrapped_transfer_tmp(deps.storage).load().is_err()); + + let token_bridge_message: TokenBridgeMessage = match transfer_type { + TransferType::WithoutPayload => TokenBridgeMessage { + action: Action::TRANSFER, + payload: transfer_info.serialize(), + }, + TransferType::WithPayload { payload } => TokenBridgeMessage { + action: Action::TRANSFER_WITH_PAYLOAD, + payload: TransferWithPayloadInfo { + transfer_info, + payload, + } + .serialize(), + }, + }; + + // Wrap up state to be captured by the submessage reply. + wrapped_transfer_tmp(deps.storage).save(&TransferState { + previous_balance: balance.balance.to_string(), + account: info.sender.to_string(), + token_address: asset, + token_canonical: asset_canonical.clone(), + message: token_bridge_message.serialize(), + multiplier: Uint128::new(multiplier).to_string(), + nonce, + })?; + } + }; + + Ok(Response::new() + .add_messages(messages) + .add_submessages(submessages) + .add_attribute("transfer.token_chain", asset_chain.to_string()) + .add_attribute("transfer.token", hex::encode(asset_address)) + .add_attribute( + "transfer.sender", + hex::encode(extend_address_to_32( + &deps.api.addr_canonicalize(info.sender.as_str())?, + )), + ) + .add_attribute("transfer.recipient_chain", recipient_chain.to_string()) + .add_attribute("transfer.recipient", hex::encode(recipient)) + .add_attribute("transfer.amount", amount.to_string()) + .add_attribute("transfer.nonce", nonce.to_string()) + .add_attribute("transfer.block_time", env.block.time.seconds().to_string())) +} + +/// All ISO-4217 currency codes are 3 letters, so we can safely slice anything that is not ULUNA. +/// https://www.xe.com/iso4217.php +fn format_native_denom_symbol(denom: &str) -> String { + if denom == "uluna" { + return "LUNA".to_string(); + } + // UUSD -> US -> UST + denom.to_uppercase()[1..3].to_string() + "T" +} + +#[allow(clippy::too_many_arguments)] +fn handle_initiate_transfer_native_token( + deps: DepsMut, + env: Env, + info: MessageInfo, + denom: String, + amount: Uint128, + recipient_chain: u16, + recipient: Vec, + fee: Uint128, + transfer_type: TransferType>, + nonce: u32, +) -> StdResult { + if recipient_chain == CHAIN_ID { + return ContractError::SameSourceAndTarget.std_err(); + } + if amount.is_zero() { + return ContractError::AmountTooLow.std_err(); + } + if fee > amount { + return Err(StdError::generic_err("fee greater than sent amount")); + } + + let deposit_key = format!("{}:{}", info.sender, denom); + bridge_deposit(deps.storage).update(deposit_key.as_bytes(), |current: Option| { + match current { + Some(v) => Ok(v.checked_sub(amount)?), + None => Err(StdError::generic_err("no deposit found to transfer")), + } + })?; + + let cfg: ConfigInfo = config_read(deps.storage).load()?; + let mut messages: Vec = vec![]; + + let asset_chain: u16 = CHAIN_ID; + let mut asset_address: Vec = build_native_id(&denom); + + send_native(deps.storage, &asset_address[..].into(), amount)?; + + // Mark the first byte of the address to distinguish it as native. + asset_address = extend_address_to_32(&asset_address.into()); + asset_address[0] = 1; + + let transfer_info = TransferInfo { + token_chain: asset_chain, + token_address: asset_address.to_vec(), + amount: (0, amount.u128()), + recipient_chain, + recipient: recipient.clone(), + fee: (0, fee.u128()), + }; + + let token_bridge_message: TokenBridgeMessage = match transfer_type { + TransferType::WithoutPayload => TokenBridgeMessage { + action: Action::TRANSFER, + payload: transfer_info.serialize(), + }, + TransferType::WithPayload { payload } => TokenBridgeMessage { + action: Action::TRANSFER_WITH_PAYLOAD, + payload: TransferWithPayloadInfo { + transfer_info, + payload, + } + .serialize(), + }, + }; + + let sender = deps.api.addr_canonicalize(info.sender.as_str())?; + messages.push(CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: cfg.wormhole_contract, + msg: to_binary(&WormholeExecuteMsg::PostMessage { + message: Binary::from(token_bridge_message.serialize()), + nonce, + })?, + funds: coins_after_tax(deps, info.funds)?, + })); + + Ok(Response::new() + .add_messages(messages) + .add_attribute("transfer.token_chain", asset_chain.to_string()) + .add_attribute("transfer.token", hex::encode(asset_address)) + .add_attribute( + "transfer.sender", + hex::encode(extend_address_to_32(&sender)), + ) + .add_attribute("transfer.recipient_chain", recipient_chain.to_string()) + .add_attribute("transfer.recipient", hex::encode(recipient)) + .add_attribute("transfer.amount", amount.to_string()) + .add_attribute("transfer.nonce", nonce.to_string()) + .add_attribute("transfer.block_time", env.block.time.seconds().to_string())) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { + match msg { + QueryMsg::WrappedRegistry { chain, address } => { + to_binary(&query_wrapped_registry(deps, chain, address.as_slice())?) + } + QueryMsg::TransferInfo { vaa } => to_binary(&query_transfer_info(deps, env, &vaa)?), + } +} + +pub fn query_wrapped_registry( + deps: Deps, + chain: u16, + address: &[u8], +) -> StdResult { + let asset_id = build_asset_id(chain, address); + // Check if this asset is already deployed + match wrapped_asset_read(deps.storage).load(&asset_id) { + Ok(address) => Ok(WrappedRegistryResponse { address }), + Err(_) => ContractError::AssetNotFound.std_err(), + } +} + +fn query_transfer_info(deps: Deps, env: Env, vaa: &Binary) -> StdResult { + let cfg = config_read(deps.storage).load()?; + + let parsed = parse_vaa(deps, env.block.time.seconds(), vaa)?; + let data = parsed.payload; + + // check if vaa is from governance + if is_governance_emitter(&cfg, parsed.emitter_chain, &parsed.emitter_address) { + return ContractError::InvalidVAAAction.std_err(); + } + + let message = TokenBridgeMessage::deserialize(&data)?; + match message.action { + Action::ATTEST_META => ContractError::InvalidVAAAction.std_err(), + _ => { + let info = TransferWithPayloadInfo::deserialize(&message.payload)?; + let core = info.transfer_info; + + Ok(TransferInfoResponse { + amount: core.amount.1.into(), + token_address: core.token_address, + token_chain: core.token_chain, + recipient: core.recipient, + recipient_chain: core.recipient_chain, + fee: core.fee.1.into(), + payload: info.payload, + }) + } + } +} + +pub fn build_asset_id(chain: u16, address: &[u8]) -> Vec { + let chain = &chain.to_be_bytes(); + let mut asset_id = Vec::with_capacity(chain.len() + address.len()); + asset_id.extend_from_slice(chain); + asset_id.extend_from_slice(address); + + let mut hasher = Keccak256::new(); + hasher.update(asset_id); + hasher.finalize().to_vec() +} + +// Produce a 20 byte asset "address" from a native terra denom. +pub fn build_native_id(denom: &str) -> Vec { + let n = denom.len(); + assert!(n < 20); + let mut asset_address = Vec::with_capacity(20); + asset_address.resize(20 - n, 0u8); + asset_address.extend_from_slice(denom.as_bytes()); + asset_address +} + +fn is_governance_emitter(cfg: &ConfigInfo, emitter_chain: u16, emitter_address: &[u8]) -> bool { + cfg.gov_chain == emitter_chain && cfg.gov_address == emitter_address +} diff --git a/wormhole/token-bridge/src/lib.rs b/wormhole/token-bridge/src/lib.rs new file mode 100644 index 00000000..e4ccd9f8 --- /dev/null +++ b/wormhole/token-bridge/src/lib.rs @@ -0,0 +1,9 @@ +#[cfg(test)] +extern crate lazy_static; + +pub mod contract; +pub mod msg; +pub mod state; + +#[cfg(test)] +mod testing; diff --git a/wormhole/token-bridge/src/msg.rs b/wormhole/token-bridge/src/msg.rs new file mode 100644 index 00000000..953ce761 --- /dev/null +++ b/wormhole/token-bridge/src/msg.rs @@ -0,0 +1,89 @@ +use cosmwasm_std::{Binary, Uint128}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use terraswap::asset::{Asset, AssetInfo}; + +type HumanAddr = String; + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +pub struct InstantiateMsg { + // governance contract details + pub gov_chain: u16, + pub gov_address: Binary, + + pub wormhole_contract: HumanAddr, + pub wrapped_asset_code_id: u64, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum ExecuteMsg { + RegisterAssetHook { + asset_id: Binary, + }, + + DepositTokens {}, + WithdrawTokens { + asset: AssetInfo, + }, + + InitiateTransfer { + asset: Asset, + recipient_chain: u16, + recipient: Binary, + fee: Uint128, + nonce: u32, + }, + + InitiateTransferWithPayload { + asset: Asset, + recipient_chain: u16, + recipient: Binary, + fee: Uint128, + payload: Binary, + nonce: u32, + }, + + SubmitVaa { + data: Binary, + }, + + CreateAssetMeta { + asset_info: AssetInfo, + nonce: u32, + }, + + CompleteTransferWithPayload { + data: Binary, + relayer: HumanAddr, + }, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub struct MigrateMsg {} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum QueryMsg { + WrappedRegistry { chain: u16, address: Binary }, + TransferInfo { vaa: Binary }, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub struct WrappedRegistryResponse { + pub address: HumanAddr, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub struct TransferInfoResponse { + pub amount: Uint128, + pub token_address: Vec, + pub token_chain: u16, + pub recipient: Vec, + pub recipient_chain: u16, + pub fee: Uint128, + pub payload: Vec, +} diff --git a/wormhole/token-bridge/src/state.rs b/wormhole/token-bridge/src/state.rs new file mode 100644 index 00000000..0f79e655 --- /dev/null +++ b/wormhole/token-bridge/src/state.rs @@ -0,0 +1,317 @@ +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use cosmwasm_std::{CanonicalAddr, StdError, StdResult, Storage, Uint128}; +use cosmwasm_storage::{ + bucket, bucket_read, singleton, singleton_read, Bucket, ReadonlyBucket, ReadonlySingleton, + Singleton, +}; + +use wormhole::byte_utils::ByteUtils; + +type HumanAddr = String; + +pub static CONFIG_KEY: &[u8] = b"config"; +pub static TRANSFER_TMP_KEY: &[u8] = b"transfer_tmp"; +pub static WRAPPED_ASSET_KEY: &[u8] = b"wrapped_asset"; +pub static WRAPPED_ASSET_SEQ_KEY: &[u8] = b"wrapped_seq_asset"; +pub static WRAPPED_ASSET_ADDRESS_KEY: &[u8] = b"wrapped_asset_address"; +pub static BRIDGE_CONTRACTS: &[u8] = b"bridge_contracts"; +pub static BRIDGE_DEPOSITS: &[u8] = b"bridge_deposits"; +pub static NATIVE_COUNTER: &[u8] = b"native_counter"; + +// Guardian set information +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +pub struct ConfigInfo { + // governance contract details + pub gov_chain: u16, + pub gov_address: Vec, + + pub wormhole_contract: HumanAddr, + pub wrapped_asset_code_id: u64, +} + +pub fn config(storage: &mut dyn Storage) -> Singleton { + singleton(storage, CONFIG_KEY) +} + +pub fn config_read(storage: &dyn Storage) -> ReadonlySingleton { + singleton_read(storage, CONFIG_KEY) +} + +pub fn bridge_deposit(storage: &mut dyn Storage) -> Bucket { + bucket(storage, BRIDGE_DEPOSITS) +} + +pub fn bridge_deposit_read(storage: &dyn Storage) -> ReadonlyBucket { + bucket_read(storage, BRIDGE_DEPOSITS) +} + +pub fn bridge_contracts(storage: &mut dyn Storage) -> Bucket> { + bucket(storage, BRIDGE_CONTRACTS) +} + +pub fn bridge_contracts_read(storage: &dyn Storage) -> ReadonlyBucket> { + bucket_read(storage, BRIDGE_CONTRACTS) +} + +pub fn wrapped_asset(storage: &mut dyn Storage) -> Bucket { + bucket(storage, WRAPPED_ASSET_KEY) +} + +pub fn wrapped_asset_read(storage: &dyn Storage) -> ReadonlyBucket { + bucket_read(storage, WRAPPED_ASSET_KEY) +} + +pub fn wrapped_asset_seq(storage: &mut dyn Storage) -> Bucket { + bucket(storage, WRAPPED_ASSET_SEQ_KEY) +} + +pub fn wrapped_asset_seq_read(storage: &mut dyn Storage) -> ReadonlyBucket { + bucket_read(storage, WRAPPED_ASSET_SEQ_KEY) +} + +pub fn wrapped_asset_address(storage: &mut dyn Storage) -> Bucket> { + bucket(storage, WRAPPED_ASSET_ADDRESS_KEY) +} + +pub fn wrapped_asset_address_read(storage: &dyn Storage) -> ReadonlyBucket> { + bucket_read(storage, WRAPPED_ASSET_ADDRESS_KEY) +} + +type Serialized128 = String; + +/// Structure to keep track of an active CW20 transfer, required to pass state through to the reply +/// handler for submessages during a transfer. +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +pub struct TransferState { + pub account: String, + pub message: Vec, + pub multiplier: Serialized128, + pub nonce: u32, + pub previous_balance: Serialized128, + pub token_address: HumanAddr, + pub token_canonical: CanonicalAddr, +} + +pub fn wrapped_transfer_tmp(storage: &mut dyn Storage) -> Singleton { + singleton(storage, TRANSFER_TMP_KEY) +} + +pub fn send_native( + storage: &mut dyn Storage, + asset_address: &CanonicalAddr, + amount: Uint128, +) -> StdResult<()> { + let mut counter_bucket = bucket(storage, NATIVE_COUNTER); + let new_total = amount + + counter_bucket + .load(asset_address.as_slice()) + .unwrap_or(Uint128::zero()); + if new_total > Uint128::new(u64::MAX as u128) { + return Err(StdError::generic_err( + "transfer exceeds max outstanding bridged token amount", + )); + } + counter_bucket.save(asset_address.as_slice(), &new_total) +} + +pub fn receive_native( + storage: &mut dyn Storage, + asset_address: &CanonicalAddr, + amount: Uint128, +) -> StdResult<()> { + let mut counter_bucket = bucket(storage, NATIVE_COUNTER); + let total: Uint128 = counter_bucket.load(asset_address.as_slice())?; + let result = total.checked_sub(amount)?; + counter_bucket.save(asset_address.as_slice(), &result) +} + +pub struct Action; + +impl Action { + pub const TRANSFER: u8 = 1; + pub const ATTEST_META: u8 = 2; + pub const TRANSFER_WITH_PAYLOAD: u8 = 3; +} + +// 0 u8 action +// 1 [u8] payload + +pub struct TokenBridgeMessage { + pub action: u8, + pub payload: Vec, +} + +impl TokenBridgeMessage { + pub fn deserialize(data: &Vec) -> StdResult { + let data = data.as_slice(); + let action = data.get_u8(0); + let payload = &data[1..]; + + Ok(TokenBridgeMessage { + action, + payload: payload.to_vec(), + }) + } + + pub fn serialize(&self) -> Vec { + [self.action.to_be_bytes().to_vec(), self.payload.clone()].concat() + } +} + +// 0 u256 amount +// 32 [u8; 32] token_address +// 64 u16 token_chain +// 66 [u8; 32] recipient +// 98 u16 recipient_chain +// 100 u256 fee + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +pub struct TransferInfo { + pub amount: (u128, u128), + pub token_address: Vec, + pub token_chain: u16, + pub recipient: Vec, + pub recipient_chain: u16, + pub fee: (u128, u128), +} + +impl TransferInfo { + pub fn deserialize(data: &[u8]) -> StdResult { + let amount = data.get_u256(0); + let token_address = data.get_bytes32(32).to_vec(); + let token_chain = data.get_u16(64); + let recipient = data.get_bytes32(66).to_vec(); + let recipient_chain = data.get_u16(98); + let fee = data.get_u256(100); + + Ok(TransferInfo { + amount, + token_address, + token_chain, + recipient, + recipient_chain, + fee, + }) + } + pub fn serialize(&self) -> Vec { + [ + self.amount.0.to_be_bytes().to_vec(), + self.amount.1.to_be_bytes().to_vec(), + self.token_address.clone(), + self.token_chain.to_be_bytes().to_vec(), + self.recipient.to_vec(), + self.recipient_chain.to_be_bytes().to_vec(), + self.fee.0.to_be_bytes().to_vec(), + self.fee.1.to_be_bytes().to_vec(), + ] + .concat() + } +} + +// 0 u256 amount +// 32 [u8; 32] token_address +// 64 u16 token_chain +// 66 [u8; 32] recipient +// 98 u16 recipient_chain +// 100 u256 fee +// 132 [u8] payload + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +pub struct TransferWithPayloadInfo { + pub transfer_info: TransferInfo, + pub payload: Vec, +} + +impl TransferWithPayloadInfo { + pub fn deserialize(data: &[u8]) -> StdResult { + let transfer_info = TransferInfo::deserialize(data)?; + let payload = TransferWithPayloadInfo::get_payload(data); + + Ok(TransferWithPayloadInfo { + transfer_info, + payload, + }) + } + pub fn serialize(&self) -> Vec { + [self.transfer_info.serialize(), self.payload.clone()].concat() + } + pub fn get_payload(data: &[u8]) -> Vec { + data[132..].to_vec() + } +} + +// 0 [32]uint8 TokenAddress +// 32 uint16 TokenChain +// 34 uint8 Decimals +// 35 [32]uint8 Symbol +// 67 [32]uint8 Name + +pub struct AssetMeta { + pub token_address: Vec, + pub token_chain: u16, + pub decimals: u8, + pub symbol: Vec, + pub name: Vec, +} + +impl AssetMeta { + pub fn deserialize(data: &Vec) -> StdResult { + let data = data.as_slice(); + let token_address = data.get_bytes32(0).to_vec(); + let token_chain = data.get_u16(32); + let decimals = data.get_u8(34); + let symbol = data.get_bytes32(35).to_vec(); + let name = data.get_bytes32(67).to_vec(); + + Ok(AssetMeta { + token_chain, + token_address, + decimals, + symbol, + name, + }) + } + + pub fn serialize(&self) -> Vec { + [ + self.token_address.clone(), + self.token_chain.to_be_bytes().to_vec(), + self.decimals.to_be_bytes().to_vec(), + self.symbol.clone(), + self.name.clone(), + ] + .concat() + } +} + +pub struct UpgradeContract { + pub new_contract: u64, +} + +pub struct RegisterChain { + pub chain_id: u16, + pub chain_address: Vec, +} + +impl UpgradeContract { + pub fn deserialize(data: &Vec) -> StdResult { + let data = data.as_slice(); + let new_contract = data.get_u64(24); + Ok(UpgradeContract { new_contract }) + } +} + +impl RegisterChain { + pub fn deserialize(data: &Vec) -> StdResult { + let data = data.as_slice(); + let chain_id = data.get_u16(0); + let chain_address = data[2..].to_vec(); + + Ok(RegisterChain { + chain_id, + chain_address, + }) + } +} diff --git a/wormhole/token-bridge/src/testing/mod.rs b/wormhole/token-bridge/src/testing/mod.rs new file mode 100644 index 00000000..14f00389 --- /dev/null +++ b/wormhole/token-bridge/src/testing/mod.rs @@ -0,0 +1 @@ +mod tests; diff --git a/wormhole/token-bridge/src/testing/tests.rs b/wormhole/token-bridge/src/testing/tests.rs new file mode 100644 index 00000000..cd26f66e --- /dev/null +++ b/wormhole/token-bridge/src/testing/tests.rs @@ -0,0 +1,184 @@ +use cosmwasm_std::{Binary, StdResult}; + +use wormhole::state::ParsedVAA; + +use crate::contract::{build_asset_id, build_native_id}; +use crate::state::{Action, TokenBridgeMessage, TransferInfo, TransferWithPayloadInfo}; + +#[test] +fn binary_check() -> StdResult<()> { + let x = vec![ + 1u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 96u8, 180u8, 94u8, 195u8, 0u8, 0u8, 0u8, + 1u8, 0u8, 3u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 38u8, 229u8, + 4u8, 215u8, 149u8, 163u8, 42u8, 54u8, 156u8, 236u8, 173u8, 168u8, 72u8, 220u8, 100u8, 90u8, + 154u8, 159u8, 160u8, 215u8, 0u8, 91u8, 48u8, 44u8, 48u8, 44u8, 51u8, 44u8, 48u8, 44u8, + 48u8, 44u8, 48u8, 44u8, 48u8, 44u8, 48u8, 44u8, 48u8, 44u8, 48u8, 44u8, 48u8, 44u8, 48u8, + 44u8, 48u8, 44u8, 48u8, 44u8, 48u8, 44u8, 53u8, 55u8, 44u8, 52u8, 54u8, 44u8, 50u8, 53u8, + 53u8, 44u8, 53u8, 48u8, 44u8, 50u8, 52u8, 51u8, 44u8, 49u8, 48u8, 54u8, 44u8, 49u8, 50u8, + 50u8, 44u8, 49u8, 49u8, 48u8, 44u8, 49u8, 50u8, 53u8, 44u8, 56u8, 56u8, 44u8, 55u8, 51u8, + 44u8, 49u8, 56u8, 57u8, 44u8, 50u8, 48u8, 55u8, 44u8, 49u8, 48u8, 52u8, 44u8, 56u8, 51u8, + 44u8, 49u8, 49u8, 57u8, 44u8, 49u8, 50u8, 55u8, 44u8, 49u8, 57u8, 50u8, 44u8, 49u8, 52u8, + 55u8, 44u8, 56u8, 57u8, 44u8, 48u8, 44u8, 48u8, 44u8, 48u8, 44u8, 48u8, 44u8, 48u8, 44u8, + 48u8, 44u8, 48u8, 44u8, 48u8, 44u8, 48u8, 44u8, 48u8, 44u8, 48u8, 44u8, 48u8, 44u8, 48u8, + 44u8, 48u8, 44u8, 48u8, 44u8, 48u8, 44u8, 48u8, 44u8, 48u8, 44u8, 48u8, 44u8, 48u8, 44u8, + 48u8, 44u8, 48u8, 44u8, 48u8, 44u8, 48u8, 44u8, 48u8, 44u8, 48u8, 44u8, 48u8, 44u8, 48u8, + 44u8, 48u8, 44u8, 48u8, 44u8, 51u8, 44u8, 50u8, 51u8, 50u8, 44u8, 48u8, 44u8, 51u8, 44u8, + 48u8, 44u8, 48u8, 44u8, 48u8, 44u8, 48u8, 44u8, 48u8, 44u8, 48u8, 44u8, 48u8, 44u8, 48u8, + 44u8, 48u8, 44u8, 48u8, 44u8, 48u8, 44u8, 48u8, 44u8, 53u8, 51u8, 44u8, 49u8, 49u8, 54u8, + 44u8, 52u8, 56u8, 44u8, 49u8, 49u8, 54u8, 44u8, 49u8, 52u8, 57u8, 44u8, 49u8, 48u8, 56u8, + 44u8, 49u8, 49u8, 51u8, 44u8, 56u8, 44u8, 48u8, 44u8, 50u8, 51u8, 50u8, 44u8, 52u8, 57u8, + 44u8, 49u8, 53u8, 50u8, 44u8, 49u8, 44u8, 50u8, 56u8, 44u8, 50u8, 48u8, 51u8, 44u8, 50u8, + 49u8, 50u8, 44u8, 50u8, 50u8, 49u8, 44u8, 50u8, 52u8, 49u8, 44u8, 56u8, 53u8, 44u8, 49u8, + 48u8, 57u8, 93u8, + ]; + let b = Binary::from(x.clone()); + let y: Vec = b.into(); + assert_eq!(x, y); + Ok(()) +} + +#[test] +fn build_native_and_asset_ids() -> StdResult<()> { + let denom = "uusd"; + let native_id = build_native_id(denom); + + let expected_native_id = vec![ + 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 117u8, + 117u8, 115u8, 100u8, + ]; + assert_eq!(&native_id, &expected_native_id, "native_id != expected"); + + // weth + let chain = 2u16; + let token_address = "000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"; + let token_address = hex::decode(token_address).unwrap(); + let asset_id = build_asset_id(chain, token_address.as_slice()); + + let expected_asset_id = vec![ + 171u8, 106u8, 233u8, 80u8, 14u8, 139u8, 124u8, 78u8, 181u8, 77u8, 142u8, 76u8, 109u8, 81u8, + 55u8, 100u8, 139u8, 159u8, 42u8, 85u8, 172u8, 234u8, 0u8, 114u8, 11u8, 82u8, 40u8, 40u8, + 50u8, 73u8, 211u8, 135u8, + ]; + assert_eq!(&asset_id, &expected_asset_id, "asset_id != expected"); + Ok(()) +} + +#[test] +fn deserialize_transfer_vaa() -> StdResult<()> { + let signed_vaa = "\ + 010000000001003f3179d5bb17b6f2ecc13741ca3f78d922043e99e09975e390\ + 4332d2418bb3f16d7ac93ca8401f8bed1cf9827bc806ecf7c5a283340f033bf4\ + 72724abf1d274f00000000000000000000010000000000000000000000000000\ + 00000000000000000000000000000000ffff0000000000000000000100000000\ + 00000000000000000000000000000000000000000000000005f5e10001000000\ + 0000000000000000000000000000000000000000000000007575736400030000\ + 00000000000000000000f7f7dde848e7450a029cd0a9bd9bdae4b5147db30003\ + 00000000000000000000000000000000000000000000000000000000000f4240"; + let signed_vaa = hex::decode(signed_vaa).unwrap(); + + let parsed = ParsedVAA::deserialize(signed_vaa.as_slice())?; + let message = TokenBridgeMessage::deserialize(&parsed.payload)?; + assert_eq!( + message.action, + Action::TRANSFER, + "message.action != expected" + ); + + let info = TransferInfo::deserialize(&message.payload)?; + + let amount = (0u128, 100_000_000u128); + assert_eq!(info.amount, amount, "info.amount != expected"); + + let token_address = "0100000000000000000000000000000000000000000000000000000075757364"; + let token_address = hex::decode(token_address).unwrap(); + assert_eq!( + info.token_address, token_address, + "info.token_address != expected" + ); + + let token_chain = 3u16; + assert_eq!( + info.token_chain, token_chain, + "info.token_chain != expected" + ); + + let recipient = "000000000000000000000000f7f7dde848e7450a029cd0a9bd9bdae4b5147db3"; + let recipient = hex::decode(recipient).unwrap(); + assert_eq!(info.recipient, recipient, "info.recipient != expected"); + + let recipient_chain = 3u16; + assert_eq!( + info.recipient_chain, recipient_chain, + "info.recipient_chain != expected" + ); + + let fee = (0u128, 1_000_000u128); + assert_eq!(info.fee, fee, "info.fee != expected"); + + Ok(()) +} + +#[test] +fn deserialize_transfer_with_payload_vaa() -> StdResult<()> { + let signed_vaa = "\ + 010000000001002b0e392ebe370e718b91dcafbba21094efd8e7f1f12e28bd90\ + a178b4dfbbc708675152a3cd2edd20e8e018600026b73b6c6cbf02622903409e\ + 8b48ab7fa30ef001000000010000000100010000000000000000000000000000\ + 00000000000000000000000000000000ffff0000000000000002000300000000\ + 00000000000000000000000000000000000000000000000005f5e10001000000\ + 0000000000000000000000000000000000000000000000007575736400030000\ + 000000000000000000008cec800d24df11e556e708461c98122df4a2c3b10003\ + 00000000000000000000000000000000000000000000000000000000000f4240\ + 416c6c20796f75722062617365206172652062656c6f6e6720746f207573"; + let signed_vaa = hex::decode(signed_vaa).unwrap(); + + let parsed = ParsedVAA::deserialize(signed_vaa.as_slice())?; + let message = TokenBridgeMessage::deserialize(&parsed.payload)?; + assert_eq!( + message.action, + Action::TRANSFER_WITH_PAYLOAD, + "message.action != expected" + ); + + let info_with_payload = TransferWithPayloadInfo::deserialize(&message.payload)?; + let info = info_with_payload.transfer_info; + + let amount = (0u128, 100_000_000u128); + assert_eq!(info.amount, amount, "info.amount != expected"); + + let token_address = "0100000000000000000000000000000000000000000000000000000075757364"; + let token_address = hex::decode(token_address).unwrap(); + assert_eq!( + info.token_address, token_address, + "info.token_address != expected" + ); + + let token_chain = 3u16; + assert_eq!( + info.token_chain, token_chain, + "info.token_chain != expected" + ); + + let recipient = "0000000000000000000000008cec800d24df11e556e708461c98122df4a2c3b1"; + let recipient = hex::decode(recipient).unwrap(); + assert_eq!(info.recipient, recipient, "info.recipient != expected"); + + let recipient_chain = 3u16; + assert_eq!( + info.recipient_chain, recipient_chain, + "info.recipient_chain != expected" + ); + + let fee = (0u128, 1_000_000u128); + assert_eq!(info.fee, fee, "info.fee != expected"); + + let transfer_payload = "All your base are belong to us"; + let transfer_payload = transfer_payload.as_bytes(); + assert_eq!( + info_with_payload.payload.as_slice(), + transfer_payload, + "info.payload != expected" + ); + + Ok(()) +} diff --git a/wormhole/wormhole/.cargo/config b/wormhole/wormhole/.cargo/config new file mode 100644 index 00000000..2d5cce4e --- /dev/null +++ b/wormhole/wormhole/.cargo/config @@ -0,0 +1,5 @@ +[alias] +wasm = "build --release --target wasm32-unknown-unknown" +wasm-debug = "build --target wasm32-unknown-unknown" +unit-test = "test --lib --features backtraces" +integration-test = "test --test integration" \ No newline at end of file diff --git a/wormhole/wormhole/Cargo.toml b/wormhole/wormhole/Cargo.toml new file mode 100644 index 00000000..45fa5477 --- /dev/null +++ b/wormhole/wormhole/Cargo.toml @@ -0,0 +1,33 @@ +[package] +name = "wormhole-bridge-terra" +version = "0.1.0" +authors = ["Yuriy Savchenko "] +edition = "2018" +description = "Wormhole contract" + +[lib] +name = "wormhole" +crate-type = ["cdylib", "rlib"] + +[features] +backtraces = ["cosmwasm-std/backtraces"] +# use library feature to disable all init/handle/query exports +library = [] + +[dependencies] +cosmwasm-std = "1.0.0" +cosmwasm-storage = "1.0.0" +cw20 = "0.13.4" +cw20-base = { version = "0.13.4", features = ["library"] } +generic-array = { version = "0.14.4" } +getrandom = { version = "0.2", features = ["custom"] } +hex = "0.4.2" +k256 = { version = "0.9.4", default-features = false, features = ["ecdsa"] } +lazy_static = "1.4.0" +schemars = "0.8.10" +serde = { version = "1.0.137", default-features = false, features = ["derive"] } +sha3 = { version = "0.9.1", default-features = false } +thiserror = "1.0.31" + +[dev-dependencies] +serde_json = "1.0" diff --git a/wormhole/wormhole/src/byte_utils.rs b/wormhole/wormhole/src/byte_utils.rs new file mode 100644 index 00000000..b31b6c85 --- /dev/null +++ b/wormhole/wormhole/src/byte_utils.rs @@ -0,0 +1,97 @@ +use cosmwasm_std::CanonicalAddr; + +pub trait ByteUtils { + fn get_u8(&self, index: usize) -> u8; + fn get_u16(&self, index: usize) -> u16; + fn get_u32(&self, index: usize) -> u32; + fn get_u64(&self, index: usize) -> u64; + + fn get_u128_be(&self, index: usize) -> u128; + /// High 128 then low 128 + fn get_u256(&self, index: usize) -> (u128, u128); + fn get_address(&self, index: usize) -> CanonicalAddr; + fn get_bytes32(&self, index: usize) -> &[u8]; + fn get_bytes(&self, index: usize, bytes: usize) -> &[u8]; + fn get_const_bytes(&self, index: usize) -> [u8; N]; +} + +impl ByteUtils for &[u8] { + fn get_u8(&self, index: usize) -> u8 { + self[index] + } + fn get_u16(&self, index: usize) -> u16 { + let mut bytes: [u8; 16 / 8] = [0; 16 / 8]; + bytes.copy_from_slice(&self[index..index + 2]); + u16::from_be_bytes(bytes) + } + fn get_u32(&self, index: usize) -> u32 { + let mut bytes: [u8; 32 / 8] = [0; 32 / 8]; + bytes.copy_from_slice(&self[index..index + 4]); + u32::from_be_bytes(bytes) + } + fn get_u64(&self, index: usize) -> u64 { + let mut bytes: [u8; 64 / 8] = [0; 64 / 8]; + bytes.copy_from_slice(&self[index..index + 8]); + u64::from_be_bytes(bytes) + } + fn get_u128_be(&self, index: usize) -> u128 { + let mut bytes: [u8; 128 / 8] = [0; 128 / 8]; + bytes.copy_from_slice(&self[index..index + 128 / 8]); + u128::from_be_bytes(bytes) + } + fn get_u256(&self, index: usize) -> (u128, u128) { + (self.get_u128_be(index), self.get_u128_be(index + 128 / 8)) + } + fn get_address(&self, index: usize) -> CanonicalAddr { + // 32 bytes are reserved for addresses, but only the last 20 bytes are taken by the actual address + CanonicalAddr::from(&self[index + 32 - 20..index + 32]) + } + fn get_bytes32(&self, index: usize) -> &[u8] { + &self[index..index + 32] + } + + fn get_bytes(&self, index: usize, bytes: usize) -> &[u8] { + &self[index..index + bytes] + } + + fn get_const_bytes(&self, index: usize) -> [u8; N] { + let mut bytes: [u8; N] = [0; N]; + bytes.copy_from_slice(&self[index..index + N]); + bytes + } +} + +/// Left-pad a 20 byte address with 0s +pub fn extend_address_to_32(addr: &CanonicalAddr) -> Vec { + extend_address_to_32_array(addr).to_vec() +} + +pub fn extend_address_to_32_array(addr: &CanonicalAddr) -> [u8; 32] { + let mut v: Vec = vec![0; 12]; + v.extend(addr.as_slice()); + let mut result: [u8; 32] = [0; 32]; + result.copy_from_slice(&v); + result +} + +/// Turn a string into a fixed length array. If the string is shorter than the +/// resulting array, it gets padded with \0s on the right. If longer, it gets +/// truncated. +pub fn string_to_array(s: &str) -> [u8; N] { + let bytes = s.as_bytes(); + let len = usize::min(N, bytes.len()); + let zeros = vec![0; N - len]; + let padded = [bytes[..len].to_vec(), zeros].concat(); + let mut result: [u8; N] = [0; N]; + result.copy_from_slice(&padded); + result +} + +pub fn extend_string_to_32(s: &str) -> Vec { + string_to_array::<32>(s).to_vec() +} + +pub fn get_string_from_32(v: &[u8]) -> String { + let s = String::from_utf8_lossy(v); + s.chars().filter(|c| c != &'\0').collect() +} diff --git a/wormhole/wormhole/src/contract.rs b/wormhole/wormhole/src/contract.rs new file mode 100644 index 00000000..74e28a29 --- /dev/null +++ b/wormhole/wormhole/src/contract.rs @@ -0,0 +1,370 @@ +use cosmwasm_std::{ + has_coins, to_binary, BankMsg, Binary, Coin, CosmosMsg, Deps, DepsMut, Env, MessageInfo, + Response, StdError, StdResult, Storage, WasmMsg, +}; + +#[cfg(not(feature = "library"))] +use cosmwasm_std::entry_point; + +use crate::byte_utils::{extend_address_to_32, ByteUtils}; +use crate::error::ContractError; +use crate::msg::{ + ExecuteMsg, GetAddressHexResponse, GetStateResponse, GuardianSetInfoResponse, InstantiateMsg, + MigrateMsg, QueryMsg, +}; +use crate::state::{ + config, config_read, guardian_set_get, guardian_set_set, sequence_read, sequence_set, + vaa_archive_add, vaa_archive_check, ConfigInfo, ContractUpgrade, GovernancePacket, + GuardianAddress, GuardianSetInfo, GuardianSetUpgrade, ParsedVAA, SetFee, TransferFee, +}; + +use k256::ecdsa::recoverable::{Id as RecoverableId, Signature as RecoverableSignature}; +use k256::ecdsa::{Signature, VerifyingKey}; +use k256::EncodedPoint; +use sha3::{Digest, Keccak256}; + +use generic_array::GenericArray; +use std::convert::TryFrom; + +type HumanAddr = String; + +// Chain ID of Terra +const CHAIN_ID: u16 = 3; + +// Lock assets fee amount and denomination +const FEE_AMOUNT: u128 = 0; +pub const FEE_DENOMINATION: &str = "uluna"; + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn migrate(_deps: DepsMut, _env: Env, _msg: MigrateMsg) -> StdResult { + Ok(Response::default()) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn instantiate( + deps: DepsMut, + _env: Env, + _info: MessageInfo, + msg: InstantiateMsg, +) -> StdResult { + // Save general wormhole info + let state = ConfigInfo { + gov_chain: msg.gov_chain, + gov_address: msg.gov_address.as_slice().to_vec(), + guardian_set_index: 0, + guardian_set_expirity: msg.guardian_set_expirity, + fee: Coin::new(FEE_AMOUNT, FEE_DENOMINATION), // 0.01 Luna (or 10000 uluna) fee by default + }; + config(deps.storage).save(&state)?; + + // Add initial guardian set to storage + guardian_set_set( + deps.storage, + state.guardian_set_index, + &msg.initial_guardian_set, + )?; + + Ok(Response::default()) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn execute(deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg) -> StdResult { + match msg { + ExecuteMsg::PostMessage { message, nonce } => { + handle_post_message(deps, env, info, message.as_slice(), nonce) + } + ExecuteMsg::SubmitVAA { vaa } => handle_submit_vaa(deps, env, info, vaa.as_slice()), + } +} + +/// Process VAA message signed by quardians +fn handle_submit_vaa( + deps: DepsMut, + env: Env, + _info: MessageInfo, + data: &[u8], +) -> StdResult { + let state = config_read(deps.storage).load()?; + + let vaa = parse_and_verify_vaa(deps.storage, data, env.block.time.seconds())?; + vaa_archive_add(deps.storage, vaa.hash.as_slice())?; + + if state.gov_chain == vaa.emitter_chain && state.gov_address == vaa.emitter_address { + if state.guardian_set_index != vaa.guardian_set_index { + return Err(StdError::generic_err( + "governance VAAs must be signed by the current guardian set", + )); + } + return handle_governance_payload(deps, env, &vaa.payload); + } + + ContractError::InvalidVAAAction.std_err() +} + +fn handle_governance_payload(deps: DepsMut, env: Env, data: &[u8]) -> StdResult { + let gov_packet = GovernancePacket::deserialize(data)?; + + let module = String::from_utf8(gov_packet.module).unwrap(); + let module: String = module.chars().filter(|c| c != &'\0').collect(); + + if module != "Core" { + return Err(StdError::generic_err("this is not a valid module")); + } + + if gov_packet.chain != 0 && gov_packet.chain != CHAIN_ID { + return Err(StdError::generic_err( + "the governance VAA is for another chain", + )); + } + + match gov_packet.action { + 1u8 => vaa_update_contract(deps, env, &gov_packet.payload), + 2u8 => vaa_update_guardian_set(deps, env, &gov_packet.payload), + 3u8 => handle_set_fee(deps, env, &gov_packet.payload), + 4u8 => handle_transfer_fee(deps, env, &gov_packet.payload), + _ => ContractError::InvalidVAAAction.std_err(), + } +} + +/// Parses raw VAA data into a struct and verifies whether it contains sufficient signatures of an +/// active guardian set i.e. is valid according to Wormhole consensus rules +fn parse_and_verify_vaa( + storage: &dyn Storage, + data: &[u8], + block_time: u64, +) -> StdResult { + let vaa = ParsedVAA::deserialize(data)?; + + if vaa.version != 1 { + return ContractError::InvalidVersion.std_err(); + } + + // Check if VAA with this hash was already accepted + if vaa_archive_check(storage, vaa.hash.as_slice()) { + return ContractError::VaaAlreadyExecuted.std_err(); + } + + // Load and check guardian set + let guardian_set = guardian_set_get(storage, vaa.guardian_set_index); + let guardian_set: GuardianSetInfo = + guardian_set.or_else(|_| ContractError::InvalidGuardianSetIndex.std_err())?; + + if guardian_set.expiration_time != 0 && guardian_set.expiration_time < block_time { + return ContractError::GuardianSetExpired.std_err(); + } + if (vaa.len_signers as usize) < guardian_set.quorum() { + return ContractError::NoQuorum.std_err(); + } + + // Verify guardian signatures + let mut last_index: i32 = -1; + let mut pos = ParsedVAA::HEADER_LEN; + + for _ in 0..vaa.len_signers { + if pos + ParsedVAA::SIGNATURE_LEN > data.len() { + return ContractError::InvalidVAA.std_err(); + } + let index = data.get_u8(pos) as i32; + if index <= last_index { + return ContractError::WrongGuardianIndexOrder.std_err(); + } + last_index = index; + + let signature = Signature::try_from( + &data[pos + ParsedVAA::SIG_DATA_POS + ..pos + ParsedVAA::SIG_DATA_POS + ParsedVAA::SIG_DATA_LEN], + ) + .or_else(|_| ContractError::CannotDecodeSignature.std_err())?; + let id = RecoverableId::new(data.get_u8(pos + ParsedVAA::SIG_RECOVERY_POS)) + .or_else(|_| ContractError::CannotDecodeSignature.std_err())?; + let recoverable_signature = RecoverableSignature::new(&signature, id) + .or_else(|_| ContractError::CannotDecodeSignature.std_err())?; + + let verify_key = recoverable_signature + .recover_verify_key_from_digest_bytes(GenericArray::from_slice(vaa.hash.as_slice())) + .or_else(|_| ContractError::CannotRecoverKey.std_err())?; + + let index = index as usize; + if index >= guardian_set.addresses.len() { + return ContractError::TooManySignatures.std_err(); + } + if !keys_equal(&verify_key, &guardian_set.addresses[index]) { + return ContractError::GuardianSignatureError.std_err(); + } + pos += ParsedVAA::SIGNATURE_LEN; + } + + Ok(vaa) +} + +fn vaa_update_guardian_set(deps: DepsMut, env: Env, data: &[u8]) -> StdResult { + /* Payload format + 0 uint32 new_index + 4 uint8 len(keys) + 5 [][20]uint8 guardian addresses + */ + + let mut state = config_read(deps.storage).load()?; + + let GuardianSetUpgrade { + new_guardian_set_index, + new_guardian_set, + } = GuardianSetUpgrade::deserialize(data)?; + + if new_guardian_set_index != state.guardian_set_index + 1 { + return ContractError::GuardianSetIndexIncreaseError.std_err(); + } + + let old_guardian_set_index = state.guardian_set_index; + + state.guardian_set_index = new_guardian_set_index; + + guardian_set_set(deps.storage, state.guardian_set_index, &new_guardian_set)?; + + config(deps.storage).save(&state)?; + + let mut old_guardian_set = guardian_set_get(deps.storage, old_guardian_set_index)?; + old_guardian_set.expiration_time = env.block.time.seconds() + state.guardian_set_expirity; + guardian_set_set(deps.storage, old_guardian_set_index, &old_guardian_set)?; + + Ok(Response::new() + .add_attribute("action", "guardian_set_change") + .add_attribute("old", old_guardian_set_index.to_string()) + .add_attribute("new", state.guardian_set_index.to_string())) +} + +fn vaa_update_contract(_deps: DepsMut, env: Env, data: &[u8]) -> StdResult { + /* Payload format + 0 [][32]uint8 new_contract + */ + + let ContractUpgrade { new_contract } = ContractUpgrade::deserialize(data)?; + + Ok(Response::new() + .add_message(CosmosMsg::Wasm(WasmMsg::Migrate { + contract_addr: env.contract.address.to_string(), + new_code_id: new_contract, + msg: to_binary(&MigrateMsg {})?, + })) + .add_attribute("action", "contract_upgrade")) +} + +pub fn handle_set_fee(deps: DepsMut, _env: Env, data: &[u8]) -> StdResult { + let set_fee_msg = SetFee::deserialize(data)?; + + // Save new fees + let mut state = config_read(deps.storage).load()?; + state.fee = set_fee_msg.fee; + config(deps.storage).save(&state)?; + + Ok(Response::new() + .add_attribute("action", "fee_change") + .add_attribute("new_fee.amount", state.fee.amount) + .add_attribute("new_fee.denom", state.fee.denom)) +} + +pub fn handle_transfer_fee(deps: DepsMut, _env: Env, data: &[u8]) -> StdResult { + let transfer_msg = TransferFee::deserialize(data)?; + + Ok(Response::new().add_message(CosmosMsg::Bank(BankMsg::Send { + to_address: deps.api.addr_humanize(&transfer_msg.recipient)?.to_string(), + amount: vec![transfer_msg.amount], + }))) +} + +fn handle_post_message( + deps: DepsMut, + env: Env, + info: MessageInfo, + message: &[u8], + nonce: u32, +) -> StdResult { + let state = config_read(deps.storage).load()?; + let fee = state.fee; + + // Check fee + if fee.amount.u128() > 0 && !has_coins(info.funds.as_ref(), &fee) { + return ContractError::FeeTooLow.std_err(); + } + + let emitter = extend_address_to_32(&deps.api.addr_canonicalize(info.sender.as_str())?); + let sequence = sequence_read(deps.storage, emitter.as_slice()); + sequence_set(deps.storage, emitter.as_slice(), sequence + 1)?; + + Ok(Response::new() + .add_attribute("message.message", hex::encode(message)) + .add_attribute("message.sender", hex::encode(emitter)) + .add_attribute("message.chain_id", CHAIN_ID.to_string()) + .add_attribute("message.nonce", nonce.to_string()) + .add_attribute("message.sequence", sequence.to_string()) + .add_attribute("message.block_time", env.block.time.seconds().to_string())) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { + match msg { + QueryMsg::GuardianSetInfo {} => to_binary(&query_guardian_set_info(deps)?), + QueryMsg::VerifyVAA { vaa, block_time } => to_binary(&query_parse_and_verify_vaa( + deps, + vaa.as_slice(), + block_time, + )?), + QueryMsg::GetState {} => to_binary(&query_state(deps)?), + QueryMsg::QueryAddressHex { address } => to_binary(&query_address_hex(deps, &address)?), + } +} + +pub fn query_guardian_set_info(deps: Deps) -> StdResult { + let state = config_read(deps.storage).load()?; + let guardian_set = guardian_set_get(deps.storage, state.guardian_set_index)?; + let res = GuardianSetInfoResponse { + guardian_set_index: state.guardian_set_index, + addresses: guardian_set.addresses, + }; + Ok(res) +} + +pub fn query_parse_and_verify_vaa( + deps: Deps, + data: &[u8], + block_time: u64, +) -> StdResult { + parse_and_verify_vaa(deps.storage, data, block_time) +} + +// returns the hex of the 32 byte address we use for some address on this chain +pub fn query_address_hex(deps: Deps, address: &HumanAddr) -> StdResult { + Ok(GetAddressHexResponse { + hex: hex::encode(extend_address_to_32(&deps.api.addr_canonicalize(address)?)), + }) +} + +pub fn query_state(deps: Deps) -> StdResult { + let state = config_read(deps.storage).load()?; + let res = GetStateResponse { fee: state.fee }; + Ok(res) +} + +fn keys_equal(a: &VerifyingKey, b: &GuardianAddress) -> bool { + let mut hasher = Keccak256::new(); + + let point = if let Some(p) = EncodedPoint::from(a).decompress() { + p + } else { + return false; + }; + + hasher.update(&point.as_bytes()[1..]); + let a = &hasher.finalize()[12..]; + + let b = &b.bytes; + if a.len() != b.len() { + return false; + } + for (ai, bi) in a.iter().zip(b.as_slice().iter()) { + if ai != bi { + return false; + } + } + true +} diff --git a/wormhole/wormhole/src/error.rs b/wormhole/wormhole/src/error.rs new file mode 100644 index 00000000..67603f5d --- /dev/null +++ b/wormhole/wormhole/src/error.rs @@ -0,0 +1,113 @@ +use cosmwasm_std::StdError; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum ContractError { + /// Invalid VAA version + #[error("InvalidVersion")] + InvalidVersion, + + /// Guardian set with this index does not exist + #[error("InvalidGuardianSetIndex")] + InvalidGuardianSetIndex, + + /// Guardian set expiration date is zero or in the past + #[error("GuardianSetExpired")] + GuardianSetExpired, + + /// Not enough signers on the VAA + #[error("NoQuorum")] + NoQuorum, + + /// Wrong guardian index order, order must be ascending + #[error("WrongGuardianIndexOrder")] + WrongGuardianIndexOrder, + + /// Some problem with signature decoding from bytes + #[error("CannotDecodeSignature")] + CannotDecodeSignature, + + /// Some problem with public key recovery from the signature + #[error("CannotRecoverKey")] + CannotRecoverKey, + + /// Recovered pubkey from signature does not match guardian address + #[error("GuardianSignatureError")] + GuardianSignatureError, + + /// VAA action code not recognized + #[error("InvalidVAAAction")] + InvalidVAAAction, + + /// VAA guardian set is not current + #[error("NotCurrentGuardianSet")] + NotCurrentGuardianSet, + + /// Only 128-bit amounts are supported + #[error("AmountTooHigh")] + AmountTooHigh, + + /// Amount should be higher than zero + #[error("AmountTooLow")] + AmountTooLow, + + /// Source and target chain ids must be different + #[error("SameSourceAndTarget")] + SameSourceAndTarget, + + /// Target chain id must be the same as the current CHAIN_ID + #[error("WrongTargetChain")] + WrongTargetChain, + + /// Wrapped asset init hook sent twice for the same asset id + #[error("AssetAlreadyRegistered")] + AssetAlreadyRegistered, + + /// Guardian set must increase in steps of 1 + #[error("GuardianSetIndexIncreaseError")] + GuardianSetIndexIncreaseError, + + /// VAA was already executed + #[error("VaaAlreadyExecuted")] + VaaAlreadyExecuted, + + /// Message sender not permitted to execute this operation + #[error("PermissionDenied")] + PermissionDenied, + + /// Could not decode target address from canonical to human-readable form + #[error("WrongTargetAddressFormat")] + WrongTargetAddressFormat, + + /// More signatures than active guardians found + #[error("TooManySignatures")] + TooManySignatures, + + /// Wrapped asset not found in the registry + #[error("AssetNotFound")] + AssetNotFound, + + /// Generic error when there is a problem with VAA structure + #[error("InvalidVAA")] + InvalidVAA, + + /// Thrown when fee is enabled for the action, but was not sent with the transaction + #[error("FeeTooLow")] + FeeTooLow, + + /// Registering asset outside of the wormhole + #[error("RegistrationForbidden")] + RegistrationForbidden, +} + +impl ContractError { + pub fn std(&self) -> StdError { + StdError::GenericErr { + msg: format!("{}", self), + } + } + + pub fn std_err(&self) -> Result { + Err(self.std()) + } +} diff --git a/wormhole/wormhole/src/lib.rs b/wormhole/wormhole/src/lib.rs new file mode 100644 index 00000000..4c573a1a --- /dev/null +++ b/wormhole/wormhole/src/lib.rs @@ -0,0 +1,10 @@ +pub mod byte_utils; +pub mod contract; +pub mod error; +pub mod msg; +pub mod state; + +pub use crate::error::ContractError; + +#[cfg(test)] +mod testing; diff --git a/wormhole/wormhole/src/msg.rs b/wormhole/wormhole/src/msg.rs new file mode 100644 index 00000000..65617221 --- /dev/null +++ b/wormhole/wormhole/src/msg.rs @@ -0,0 +1,61 @@ +use cosmwasm_std::{Binary, Coin}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use crate::state::{GuardianAddress, GuardianSetInfo}; + +type HumanAddr = String; + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +pub struct InstantiateMsg { + pub gov_chain: u16, + pub gov_address: Binary, + + pub initial_guardian_set: GuardianSetInfo, + pub guardian_set_expirity: u64, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum ExecuteMsg { + SubmitVAA { vaa: Binary }, + PostMessage { message: Binary, nonce: u32 }, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub struct MigrateMsg {} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum QueryMsg { + GuardianSetInfo {}, + VerifyVAA { vaa: Binary, block_time: u64 }, + GetState {}, + QueryAddressHex { address: HumanAddr }, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub struct GuardianSetInfoResponse { + pub guardian_set_index: u32, // Current guardian set index + pub addresses: Vec, // List of querdian addresses +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub struct WrappedRegistryResponse { + pub address: HumanAddr, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub struct GetStateResponse { + pub fee: Coin, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub struct GetAddressHexResponse { + pub hex: String, +} diff --git a/wormhole/wormhole/src/state.rs b/wormhole/wormhole/src/state.rs new file mode 100644 index 00000000..6d329074 --- /dev/null +++ b/wormhole/wormhole/src/state.rs @@ -0,0 +1,360 @@ +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use cosmwasm_std::{Binary, CanonicalAddr, Coin, StdResult, Storage, Uint128}; +use cosmwasm_storage::{ + bucket, bucket_read, singleton, singleton_read, Bucket, ReadonlyBucket, ReadonlySingleton, + Singleton, +}; + +use crate::byte_utils::ByteUtils; +use crate::error::ContractError; + +use sha3::{Digest, Keccak256}; + +type HumanAddr = String; + +pub static CONFIG_KEY: &[u8] = b"config"; +pub static GUARDIAN_SET_KEY: &[u8] = b"guardian_set"; +pub static SEQUENCE_KEY: &[u8] = b"sequence"; +pub static WRAPPED_ASSET_KEY: &[u8] = b"wrapped_asset"; +pub static WRAPPED_ASSET_ADDRESS_KEY: &[u8] = b"wrapped_asset_address"; + +// Guardian set information +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +pub struct ConfigInfo { + // Current active guardian set + pub guardian_set_index: u32, + + // Period for which a guardian set stays active after it has been replaced + pub guardian_set_expirity: u64, + + // governance contract details + pub gov_chain: u16, + pub gov_address: Vec, + + // Message sending fee + pub fee: Coin, +} + +// Validator Action Approval(VAA) data +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +pub struct ParsedVAA { + pub version: u8, + pub guardian_set_index: u32, + pub timestamp: u32, + pub nonce: u32, + pub len_signers: u8, + + pub emitter_chain: u16, + pub emitter_address: Vec, + pub sequence: u64, + pub consistency_level: u8, + pub payload: Vec, + + pub hash: Vec, +} + +impl ParsedVAA { + /* VAA format: + + header (length 6): + 0 uint8 version (0x01) + 1 uint32 guardian set index + 5 uint8 len signatures + + per signature (length 66): + 0 uint8 index of the signer (in guardian keys) + 1 [65]uint8 signature + + body: + 0 uint32 timestamp (unix in seconds) + 4 uint32 nonce + 8 uint16 emitter_chain + 10 [32]uint8 emitter_address + 42 uint64 sequence + 50 uint8 consistency_level + 51 []uint8 payload + */ + + pub const HEADER_LEN: usize = 6; + pub const SIGNATURE_LEN: usize = 66; + + pub const GUARDIAN_SET_INDEX_POS: usize = 1; + pub const LEN_SIGNER_POS: usize = 5; + + pub const VAA_NONCE_POS: usize = 4; + pub const VAA_EMITTER_CHAIN_POS: usize = 8; + pub const VAA_EMITTER_ADDRESS_POS: usize = 10; + pub const VAA_SEQUENCE_POS: usize = 42; + pub const VAA_CONSISTENCY_LEVEL_POS: usize = 50; + pub const VAA_PAYLOAD_POS: usize = 51; + + // Signature data offsets in the signature block + pub const SIG_DATA_POS: usize = 1; + // Signature length minus recovery id at the end + pub const SIG_DATA_LEN: usize = 64; + // Recovery byte is last after the main signature + pub const SIG_RECOVERY_POS: usize = Self::SIG_DATA_POS + Self::SIG_DATA_LEN; + + pub fn deserialize(data: &[u8]) -> StdResult { + let version = data.get_u8(0); + + // Load 4 bytes starting from index 1 + let guardian_set_index: u32 = data.get_u32(Self::GUARDIAN_SET_INDEX_POS); + let len_signers = data.get_u8(Self::LEN_SIGNER_POS) as usize; + let body_offset: usize = Self::HEADER_LEN + Self::SIGNATURE_LEN * len_signers as usize; + + // Hash the body + if body_offset >= data.len() { + return ContractError::InvalidVAA.std_err(); + } + let body = &data[body_offset..]; + let mut hasher = Keccak256::new(); + hasher.update(body); + let hash = hasher.finalize().to_vec(); + + // Rehash the hash + let mut hasher = Keccak256::new(); + hasher.update(hash); + let hash = hasher.finalize().to_vec(); + + // Signatures valid, apply VAA + if body_offset + Self::VAA_PAYLOAD_POS > data.len() { + return ContractError::InvalidVAA.std_err(); + } + + let timestamp = data.get_u32(body_offset); + let nonce = data.get_u32(body_offset + Self::VAA_NONCE_POS); + let emitter_chain = data.get_u16(body_offset + Self::VAA_EMITTER_CHAIN_POS); + let emitter_address = data + .get_bytes32(body_offset + Self::VAA_EMITTER_ADDRESS_POS) + .to_vec(); + let sequence = data.get_u64(body_offset + Self::VAA_SEQUENCE_POS); + let consistency_level = data.get_u8(body_offset + Self::VAA_CONSISTENCY_LEVEL_POS); + let payload = data[body_offset + Self::VAA_PAYLOAD_POS..].to_vec(); + + Ok(ParsedVAA { + version, + guardian_set_index, + timestamp, + nonce, + len_signers: len_signers as u8, + emitter_chain, + emitter_address, + sequence, + consistency_level, + payload, + hash, + }) + } +} + +// Guardian address +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +pub struct GuardianAddress { + pub bytes: Binary, // 20-byte addresses +} + +use crate::contract::FEE_DENOMINATION; +#[cfg(test)] +use hex; + +#[cfg(test)] +impl GuardianAddress { + pub fn from(string: &str) -> GuardianAddress { + GuardianAddress { + bytes: hex::decode(string).expect("Decoding failed").into(), + } + } +} + +// Guardian set information +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +pub struct GuardianSetInfo { + pub addresses: Vec, + // List of guardian addresses + pub expiration_time: u64, // Guardian set expiration time +} + +impl GuardianSetInfo { + pub fn quorum(&self) -> usize { + // allow quorum of 0 for testing purposes... + if self.addresses.is_empty() { + return 0; + } + ((self.addresses.len() * 10 / 3) * 2) / 10 + 1 + } +} + +// Wormhole contract generic information +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +pub struct WormholeInfo { + // Period for which a guardian set stays active after it has been replaced + pub guardian_set_expirity: u64, +} + +pub fn config(storage: &mut dyn Storage) -> Singleton { + singleton(storage, CONFIG_KEY) +} + +pub fn config_read(storage: &dyn Storage) -> ReadonlySingleton { + singleton_read(storage, CONFIG_KEY) +} + +pub fn guardian_set_set( + storage: &mut dyn Storage, + index: u32, + data: &GuardianSetInfo, +) -> StdResult<()> { + bucket(storage, GUARDIAN_SET_KEY).save(&index.to_be_bytes(), data) +} + +pub fn guardian_set_get(storage: &dyn Storage, index: u32) -> StdResult { + bucket_read(storage, GUARDIAN_SET_KEY).load(&index.to_be_bytes()) +} + +pub fn sequence_set(storage: &mut dyn Storage, emitter: &[u8], sequence: u64) -> StdResult<()> { + bucket(storage, SEQUENCE_KEY).save(emitter, &sequence) +} + +pub fn sequence_read(storage: &dyn Storage, emitter: &[u8]) -> u64 { + bucket_read(storage, SEQUENCE_KEY) + .load(emitter) + .unwrap_or_default() +} + +pub fn vaa_archive_add(storage: &mut dyn Storage, hash: &[u8]) -> StdResult<()> { + bucket(storage, GUARDIAN_SET_KEY).save(hash, &true) +} + +pub fn vaa_archive_check(storage: &dyn Storage, hash: &[u8]) -> bool { + bucket_read(storage, GUARDIAN_SET_KEY) + .load(hash) + .unwrap_or_default() +} + +pub fn wrapped_asset(storage: &mut dyn Storage) -> Bucket { + bucket(storage, WRAPPED_ASSET_KEY) +} + +pub fn wrapped_asset_read(storage: &dyn Storage) -> ReadonlyBucket { + bucket_read(storage, WRAPPED_ASSET_KEY) +} + +pub fn wrapped_asset_address(storage: &mut dyn Storage) -> Bucket> { + bucket(storage, WRAPPED_ASSET_ADDRESS_KEY) +} + +pub fn wrapped_asset_address_read(storage: &dyn Storage) -> ReadonlyBucket> { + bucket_read(storage, WRAPPED_ASSET_ADDRESS_KEY) +} + +pub struct GovernancePacket { + pub module: Vec, + pub action: u8, + pub chain: u16, + pub payload: Vec, +} + +impl GovernancePacket { + pub fn deserialize(data: &[u8]) -> StdResult { + let module = data.get_bytes32(0).to_vec(); + let action = data.get_u8(32); + let chain = data.get_u16(33); + let payload = data[35..].to_vec(); + + Ok(GovernancePacket { + module, + action, + chain, + payload, + }) + } +} + +// action 1 +pub struct ContractUpgrade { + pub new_contract: u64, +} + +// action 2 +pub struct GuardianSetUpgrade { + pub new_guardian_set_index: u32, + pub new_guardian_set: GuardianSetInfo, +} + +impl ContractUpgrade { + pub fn deserialize(data: &[u8]) -> StdResult { + let new_contract = data.get_u64(24); + Ok(ContractUpgrade { new_contract }) + } +} + +impl GuardianSetUpgrade { + pub fn deserialize(data: &[u8]) -> StdResult { + const ADDRESS_LEN: usize = 20; + + let new_guardian_set_index = data.get_u32(0); + + let n_guardians = data.get_u8(4); + + let mut addresses = vec![]; + + for i in 0..n_guardians { + let pos = 5 + (i as usize) * ADDRESS_LEN; + if pos + ADDRESS_LEN > data.len() { + return ContractError::InvalidVAA.std_err(); + } + + addresses.push(GuardianAddress { + bytes: data[pos..pos + ADDRESS_LEN].to_vec().into(), + }); + } + + let new_guardian_set = GuardianSetInfo { + addresses, + expiration_time: 0, + }; + + Ok(GuardianSetUpgrade { + new_guardian_set_index, + new_guardian_set, + }) + } +} + +// action 3 +pub struct SetFee { + pub fee: Coin, +} + +impl SetFee { + pub fn deserialize(data: &[u8]) -> StdResult { + let (_, amount) = data.get_u256(0); + let fee = Coin { + denom: String::from(FEE_DENOMINATION), + amount: Uint128::new(amount), + }; + Ok(SetFee { fee }) + } +} + +// action 4 +pub struct TransferFee { + pub amount: Coin, + pub recipient: CanonicalAddr, +} + +impl TransferFee { + pub fn deserialize(data: &[u8]) -> StdResult { + let recipient = data.get_address(0); + + let (_, amount) = data.get_u256(32); + let amount = Coin { + denom: String::from(FEE_DENOMINATION), + amount: Uint128::new(amount), + }; + Ok(TransferFee { amount, recipient }) + } +} diff --git a/wormhole/wormhole/src/testing/mod.rs b/wormhole/wormhole/src/testing/mod.rs new file mode 100644 index 00000000..14f00389 --- /dev/null +++ b/wormhole/wormhole/src/testing/mod.rs @@ -0,0 +1 @@ +mod tests; diff --git a/wormhole/wormhole/src/testing/tests.rs b/wormhole/wormhole/src/testing/tests.rs new file mode 100644 index 00000000..56d6b0c3 --- /dev/null +++ b/wormhole/wormhole/src/testing/tests.rs @@ -0,0 +1,175 @@ +use cosmwasm_std::StdResult; + +use crate::state::{GuardianAddress, GuardianSetInfo, ParsedVAA}; + +#[test] +fn quardian_set_quorum() { + let num_guardians_trials: Vec = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 20, 25, 100]; + + let expected_quorums: Vec = vec![1, 2, 3, 3, 4, 5, 5, 6, 7, 7, 8, 9, 14, 17, 67]; + + let make_guardian_set = |n: usize| -> GuardianSetInfo { + let mut addresses = Vec::with_capacity(n); + for _ in 0..n { + addresses.push(GuardianAddress { + bytes: Vec::new().into(), + }); + } + GuardianSetInfo { + addresses, + expiration_time: 0, + } + }; + + for (i, &num_guardians) in num_guardians_trials.iter().enumerate() { + let quorum = make_guardian_set(num_guardians).quorum(); + assert_eq!(quorum, expected_quorums[i], "quorum != expected"); + } +} + +#[test] +fn deserialize_round_1() -> StdResult<()> { + let signed_vaa = "\ + 080000000901007bfa71192f886ab6819fa4862e34b4d178962958d9b2e3d943\ + 7338c9e5fde1443b809d2886eaa69e0f0158ea517675d96243c9209c3fe1d94d\ + 5b19866654c6980000000b150000000500020001020304000000000000000000\ + 000000000000000000000000000000000000000000000000000a0261626364"; + let signed_vaa = hex::decode(signed_vaa).unwrap(); + + let parsed = ParsedVAA::deserialize(signed_vaa.as_slice())?; + + let version = 8u8; + assert_eq!(parsed.version, version, "parsed.version != expected"); + + let guardian_set_index = 9u32; + assert_eq!( + parsed.guardian_set_index, guardian_set_index, + "parsed.guardian_set_index != expected" + ); + + let timestamp = 2837u32; + assert_eq!(parsed.timestamp, timestamp, "parsed.timestamp != expected"); + + let nonce = 5u32; + assert_eq!(parsed.nonce, nonce, "parsed.nonce != expected"); + + let len_signers = 1u8; + assert_eq!( + parsed.len_signers, len_signers, + "parsed.len_signers != expected" + ); + + let emitter_chain = 2u16; + assert_eq!( + parsed.emitter_chain, emitter_chain, + "parsed.emitter_chain != expected" + ); + + let emitter_address = "0001020304000000000000000000000000000000000000000000000000000000"; + let emitter_address = hex::decode(emitter_address).unwrap(); + assert_eq!( + parsed.emitter_address, emitter_address, + "parsed.emitter_address != expected" + ); + + let sequence = 10u64; + assert_eq!(parsed.sequence, sequence, "parsed.sequence != expected"); + + let consistency_level = 2u8; + assert_eq!( + parsed.consistency_level, consistency_level, + "parsed.consistency_level != expected" + ); + + let payload = vec![97u8, 98u8, 99u8, 100u8]; + assert_eq!(parsed.payload, payload, "parsed.payload != expected"); + + let hash = vec![ + 164u8, 44u8, 82u8, 103u8, 33u8, 170u8, 183u8, 178u8, 188u8, 204u8, 35u8, 53u8, 78u8, 148u8, + 160u8, 153u8, 122u8, 252u8, 84u8, 211u8, 26u8, 204u8, 128u8, 215u8, 37u8, 232u8, 222u8, + 186u8, 222u8, 186u8, 98u8, 94u8, + ]; + assert_eq!(parsed.hash, hash, "parsed.hash != expected"); + + Ok(()) +} + +#[test] +fn deserialize_round_2() -> StdResult<()> { + let signed_vaa = "\ + 010000000001003f3179d5bb17b6f2ecc13741ca3f78d922043e99e09975e390\ + 4332d2418bb3f16d7ac93ca8401f8bed1cf9827bc806ecf7c5a283340f033bf4\ + 72724abf1d274f00000000000000000000010000000000000000000000000000\ + 00000000000000000000000000000000ffff0000000000000000000100000000\ + 00000000000000000000000000000000000000000000000005f5e10001000000\ + 0000000000000000000000000000000000000000000000007575736400030000\ + 00000000000000000000f7f7dde848e7450a029cd0a9bd9bdae4b5147db30003\ + 00000000000000000000000000000000000000000000000000000000000f4240"; + let signed_vaa = hex::decode(signed_vaa).unwrap(); + + let parsed = ParsedVAA::deserialize(signed_vaa.as_slice())?; + + let version = 1u8; + assert_eq!(parsed.version, version, "parsed.version != expected"); + + let guardian_set_index = 0u32; + assert_eq!( + parsed.guardian_set_index, guardian_set_index, + "parsed.guardian_set_index != expected" + ); + + let timestamp = 0u32; + assert_eq!(parsed.timestamp, timestamp, "parsed.timestamp != expected"); + + let nonce = 0u32; + assert_eq!(parsed.nonce, nonce, "parsed.nonce != expected"); + + let len_signers = 1u8; + assert_eq!( + parsed.len_signers, len_signers, + "parsed.len_signers != expected" + ); + + let emitter_chain = 1u16; + assert_eq!( + parsed.emitter_chain, emitter_chain, + "parsed.emitter_chain != expected" + ); + + let emitter_address = "000000000000000000000000000000000000000000000000000000000000ffff"; + let emitter_address = hex::decode(emitter_address).unwrap(); + assert_eq!( + parsed.emitter_address, emitter_address, + "parsed.emitter_address != expected" + ); + + let sequence = 0u64; + assert_eq!(parsed.sequence, sequence, "parsed.sequence != expected"); + + let consistency_level = 0u8; + assert_eq!( + parsed.consistency_level, consistency_level, + "parsed.consistency_level != expected" + ); + + let payload = vec![ + 1u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, + 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 5u8, 245u8, 225u8, 0u8, 1u8, 0u8, + 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, + 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 117u8, 117u8, 115u8, 100u8, 0u8, 3u8, 0u8, 0u8, + 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 247u8, 247u8, 221u8, 232u8, 72u8, 231u8, + 69u8, 10u8, 2u8, 156u8, 208u8, 169u8, 189u8, 155u8, 218u8, 228u8, 181u8, 20u8, 125u8, + 179u8, 0u8, 3u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, + 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 15u8, 66u8, 64u8, + ]; + assert_eq!(parsed.payload, payload, "parsed.payload != expected"); + + let hash = vec![ + 114u8, 108u8, 111u8, 78u8, 204u8, 83u8, 150u8, 170u8, 240u8, 15u8, 193u8, 176u8, 165u8, + 87u8, 174u8, 230u8, 94u8, 222u8, 106u8, 206u8, 179u8, 203u8, 193u8, 187u8, 1u8, 148u8, + 17u8, 40u8, 248u8, 214u8, 147u8, 68u8, + ]; + assert_eq!(parsed.hash, hash, "parsed.hash != expected"); + + Ok(()) +} diff --git a/wormhole/wormhole/tests/integration.rs b/wormhole/wormhole/tests/integration.rs new file mode 100644 index 00000000..2247ebad --- /dev/null +++ b/wormhole/wormhole/tests/integration.rs @@ -0,0 +1,58 @@ +use cosmwasm_std::testing::{ + mock_dependencies, mock_env, mock_info, MockApi, MockQuerier, MockStorage, +}; +use cosmwasm_std::{from_slice, Coin, OwnedDeps, Response, Storage}; +use cosmwasm_storage::to_length_prefixed; + +use wormhole::contract::instantiate; +use wormhole::msg::InstantiateMsg; +use wormhole::state::{ConfigInfo, GuardianAddress, GuardianSetInfo, CONFIG_KEY}; + +static INITIALIZER: &str = "initializer"; +static GOV_ADDR: &[u8] = b"GOVERNANCE_ADDRESS"; + +fn get_config_info(storage: &S) -> ConfigInfo { + let key = to_length_prefixed(CONFIG_KEY); + let data = storage.get(&key).expect("data should exist"); + from_slice(&data).expect("invalid data") +} + +fn do_init(guardians: &[GuardianAddress]) -> OwnedDeps { + let mut deps = mock_dependencies(); + let init_msg = InstantiateMsg { + gov_chain: 0, + gov_address: GOV_ADDR.into(), + initial_guardian_set: GuardianSetInfo { + addresses: guardians.to_vec(), + expiration_time: 100, + }, + guardian_set_expirity: 50, + }; + let env = mock_env(); + let info = mock_info(INITIALIZER, &[]); + let res: Response = instantiate(deps.as_mut(), env, info, init_msg).unwrap(); + assert_eq!(0, res.messages.len()); + + // query the store directly + assert_eq!( + get_config_info(&deps.storage), + ConfigInfo { + guardian_set_index: 0, + guardian_set_expirity: 50, + gov_chain: 0, + gov_address: GOV_ADDR.to_vec(), + fee: Coin::new(0, "uluna"), + } + ); + deps +} + +#[test] +fn init_works() { + let guardians = [GuardianAddress { + bytes: hex::decode("beFA429d57cD18b7F8A4d91A2da9AB4AF05d0FBe") + .expect("Decoding failed") + .into(), + }]; + let _deps = do_init(&guardians); +}