From 3f4ca9ca3aa93b1b0e727e69469b980ecca848ca Mon Sep 17 00:00:00 2001 From: Interchain Adair <32375605+adairrr@users.noreply.github.com> Date: Sat, 11 May 2024 03:27:46 -0700 Subject: [PATCH] Initial commit --- .cargo/config.toml | 7 + .editorconfig | 11 + .gitattributes | 1 + .github/dependabot.yml | 17 + .github/workflows/check.yml | 93 +++++ .github/workflows/scheduled.yml | 48 +++ .github/workflows/test.yml | 27 ++ .gitignore | 14 + Cargo.toml | 63 ++++ README.md | 113 ++++++ example.env | 8 + examples/local_daemon.rs | 75 ++++ examples/publish.rs | 68 ++++ examples/schema.rs | 14 + justfile | 132 +++++++ metadata.json | 9 + rustfmt.toml | 4 + schema/execute_msg.json | 393 ++++++++++++++++++++ schema/instantiate_msg.json | 87 +++++ schema/migrate_msg.json | 38 ++ schema/module-schema.json | 143 +++++++ schema/query_msg.json | 125 +++++++ schema/raw/execute.json | 58 +++ schema/raw/instantiate.json | 17 + schema/raw/migrate.json | 6 + schema/raw/query.json | 33 ++ schema/raw/response_to_base_admin.json | 15 + schema/raw/response_to_base_config.json | 28 ++ schema/raw/response_to_config.json | 6 + schema/raw/response_to_count.json | 15 + schema/raw/response_to_module_data.json | 52 +++ schema/raw/response_to_top_level_owner.json | 20 + src/contract.rs | 34 ++ src/error.rs | 28 ++ src/handlers/execute.rs | 43 +++ src/handlers/instantiate.rs | 21 ++ src/handlers/migrate.rs | 10 + src/handlers/mod.rs | 9 + src/handlers/query.rs | 22 ++ src/lib.rs | 11 + src/msg.rs | 51 +++ src/replies/instantiate.rs | 8 + src/replies/mod.rs | 5 + src/state.rs | 7 + template-setup.sh | 37 ++ tests/integration.rs | 89 +++++ 46 files changed, 2115 insertions(+) create mode 100644 .cargo/config.toml create mode 100644 .editorconfig create mode 100644 .gitattributes create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/check.yml create mode 100644 .github/workflows/scheduled.yml create mode 100644 .github/workflows/test.yml create mode 100644 .gitignore create mode 100644 Cargo.toml create mode 100644 README.md create mode 100644 example.env create mode 100644 examples/local_daemon.rs create mode 100644 examples/publish.rs create mode 100644 examples/schema.rs create mode 100644 justfile create mode 100644 metadata.json create mode 100644 rustfmt.toml create mode 100644 schema/execute_msg.json create mode 100644 schema/instantiate_msg.json create mode 100644 schema/migrate_msg.json create mode 100644 schema/module-schema.json create mode 100644 schema/query_msg.json create mode 100644 schema/raw/execute.json create mode 100644 schema/raw/instantiate.json create mode 100644 schema/raw/migrate.json create mode 100644 schema/raw/query.json create mode 100644 schema/raw/response_to_base_admin.json create mode 100644 schema/raw/response_to_base_config.json create mode 100644 schema/raw/response_to_config.json create mode 100644 schema/raw/response_to_count.json create mode 100644 schema/raw/response_to_module_data.json create mode 100644 schema/raw/response_to_top_level_owner.json create mode 100644 src/contract.rs create mode 100644 src/error.rs create mode 100644 src/handlers/execute.rs create mode 100644 src/handlers/instantiate.rs create mode 100644 src/handlers/migrate.rs create mode 100644 src/handlers/mod.rs create mode 100644 src/handlers/query.rs create mode 100644 src/lib.rs create mode 100644 src/msg.rs create mode 100644 src/replies/instantiate.rs create mode 100644 src/replies/mod.rs create mode 100644 src/state.rs create mode 100755 template-setup.sh create mode 100644 tests/integration.rs diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..bc08ae8 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,7 @@ +[alias] +wasm = "build --release --target wasm32-unknown-unknown" +unit-test = "test --lib" +schema = "run --example schema --features schema" + +publish = "run --example publish --features daemon" +local_daemon = "run --example local_daemon --features daemon" diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..3d36f20 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,11 @@ +root = true + +[*] +indent_style = space +indent_size = 2 +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.rs] +indent_size = 4 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..7da3839 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +schema/*.json linguist-generated=true diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..8139a93 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,17 @@ +version: 2 +updates: + - package-ecosystem: github-actions + directory: / + schedule: + interval: daily + - package-ecosystem: cargo + directory: / + schedule: + interval: daily + ignore: + - dependency-name: "*" + # patch and minor updates don't matter for libraries + # remove this ignore rule if your package has binaries + update-types: + - "version-update:semver-patch" + - "version-update:semver-minor" diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml new file mode 100644 index 0000000..b9650a8 --- /dev/null +++ b/.github/workflows/check.yml @@ -0,0 +1,93 @@ +permissions: + contents: read +on: + push: + branches: [main] + pull_request: +name: check +jobs: + fmt: + runs-on: ubuntu-latest + name: stable / fmt + steps: + - uses: actions/checkout@v4 + with: + submodules: true + - name: Install stable + uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt + - name: cargo fmt --check + run: cargo fmt --check + - name: install taplo-cli + run: cargo install taplo-cli --locked + - name: taplo fmt --check + run: find . -type f -iname "*.toml" -print0 | xargs -0 taplo format --check + clippy: + runs-on: ubuntu-latest + name: ${{ matrix.toolchain }} / clippy + permissions: + contents: read + checks: write + strategy: + fail-fast: false + matrix: + toolchain: [stable, beta] + steps: + - uses: actions/checkout@v4 + with: + submodules: true + - name: Install ${{ matrix.toolchain }} + uses: dtolnay/rust-toolchain@master + with: + toolchain: ${{ matrix.toolchain }} + components: clippy + - name: cargo clippy + uses: actions-rs/clippy-check@v1 + with: + token: ${{ secrets.GITHUB_TOKEN }} + args: --all-features -- -D warnings + doc: + runs-on: ubuntu-latest + name: nightly / doc + steps: + - uses: actions/checkout@v4 + with: + submodules: true + - name: Install nightly + uses: dtolnay/rust-toolchain@nightly + - name: cargo doc + run: cargo doc --no-deps --all-features + env: + RUSTDOCFLAGS: --cfg docsrs + hack: + runs-on: ubuntu-latest + name: ubuntu / stable / features + steps: + - uses: actions/checkout@v4 + with: + submodules: true + - name: Install stable + uses: dtolnay/rust-toolchain@stable + - name: cargo install cargo-hack + uses: taiki-e/install-action@cargo-hack + - name: cargo hack + run: cargo hack --feature-powerset check --lib --tests + msrv: + runs-on: ubuntu-latest + # we use a matrix here just because env can't be used in job names + # https://docs.github.com/en/actions/learn-github-actions/contexts#context-availability + strategy: + matrix: + msrv: [1.72.0] # cosmrs v0.15.0 requires 1.72.0 or newer + name: ubuntu / ${{ matrix.msrv }} + steps: + - uses: actions/checkout@v4 + with: + submodules: true + - name: Install ${{ matrix.msrv }} + uses: dtolnay/rust-toolchain@master + with: + toolchain: ${{ matrix.msrv }} + - name: cargo +${{ matrix.msrv }} check + run: cargo check diff --git a/.github/workflows/scheduled.yml b/.github/workflows/scheduled.yml new file mode 100644 index 0000000..02fb6c5 --- /dev/null +++ b/.github/workflows/scheduled.yml @@ -0,0 +1,48 @@ +permissions: + contents: read +on: + push: + branches: [main] + pull_request: + schedule: + - cron: "7 7 * * *" +name: rolling +jobs: + # https://twitter.com/mycoliza/status/1571295690063753218 + nightly: + runs-on: ubuntu-latest + name: ubuntu / nightly + steps: + - uses: actions/checkout@v4 + with: + submodules: true + - name: Install nightly + uses: dtolnay/rust-toolchain@nightly + - name: cargo generate-lockfile + if: hashFiles('Cargo.lock') == '' + run: cargo generate-lockfile + - name: cargo test --locked + run: cargo test --locked --all-features --all-targets + # https://twitter.com/alcuadrado/status/1571291687837732873 + update: + runs-on: ubuntu-latest + name: ubuntu / beta / updated + # There's no point running this if no Cargo.lock was checked in in the + # first place, since we'd just redo what happened in the regular test job. + # Unfortunately, hashFiles only works in if on steps, so we reepeat it. + # if: hashFiles('Cargo.lock') != '' + steps: + - uses: actions/checkout@v4 + with: + submodules: true + - name: Install beta + if: hashFiles('Cargo.lock') != '' + uses: dtolnay/rust-toolchain@beta + - name: cargo update + if: hashFiles('Cargo.lock') != '' + run: cargo update + - name: cargo test + if: hashFiles('Cargo.lock') != '' + run: cargo test --locked --all-features --all-targets + env: + RUSTFLAGS: -D deprecated diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..9a087ef --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,27 @@ +permissions: + contents: read +on: + push: + branches: [main] + pull_request: +name: test +jobs: + required: + runs-on: ubuntu-latest + name: ubuntu / ${{ matrix.toolchain }} + strategy: + matrix: + toolchain: [stable, beta] + steps: + - uses: actions/checkout@v4 + with: + submodules: true + - name: Install ${{ matrix.toolchain }} + uses: dtolnay/rust-toolchain@master + with: + toolchain: ${{ matrix.toolchain }} + - name: cargo generate-lockfile + if: hashFiles('Cargo.lock') == '' + run: cargo generate-lockfile + - name: cargo test --locked + run: cargo test --locked --all-features --all-targets diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f39c1b2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +/target + +artifacts + +Cargo.lock + +.env + +.vscode +.idea + +state.json + +typescript/node_modules \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..41f9a80 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,63 @@ +[package] +name = "app" +version = "0.0.1" +authors = [ + "CyberHoward ", + "Adair ", + "Abstract Money ", +] +edition = "2021" +homepage = "" +documentation = "" +repository = "" +license = "GPL-3.0-or-later" +keywords = ["cosmos", "cosmwasm", "abstractsdk"] +resolver = "2" + +exclude = ["contract.wasm", "hash.txt"] + +[lib] +crate-type = ["cdylib", "rlib"] + +[[example]] +name = "schema" +required-features = ["schema"] + +[[example]] +name = "local_daemon" +required-features = ["daemon"] + +[[example]] +name = "publish" +required-features = ["daemon"] + +[features] +default = ["export"] +export = [] +schema = ["abstract-app/schema"] +interface = ["export", "abstract-app/interface-macro", "dep:cw-orch"] +daemon = ["interface", "cw-orch/daemon"] + +[dependencies] +cosmwasm-std = { version = "1.5.3" } +cosmwasm-schema = { version = "1.5.3" } +cw-controllers = { version = "1.1.2" } +cw-storage-plus = "1.2.0" +thiserror = { version = "1.0.50" } +schemars = "0.8" +cw-asset = { version = "3.0.0" } +abstract-app = { version = "0.21.0" } + +# Dependencies for interface +cw-orch = { version = "0.20.1", optional = true } + +[dev-dependencies] +app = { path = ".", features = ["interface"] } +abstract-client = { version = "0.21.0" } +abstract-app = { version = "0.21", features = ["test-utils"] } +speculoos = "0.11.0" +semver = "1.0" +dotenv = "0.15.0" +env_logger = "0.10.0" +cw-orch = { version = "0.20.1" } +clap = { version = "4.3.7", features = ["derive"] } diff --git a/README.md b/README.md new file mode 100644 index 0000000..8b5c678 --- /dev/null +++ b/README.md @@ -0,0 +1,113 @@ +# Abstract App Module Template + +The Abstract App Module Template is a starting point for developing composable smart-contracts, or "Apps" on the Abstract platform. An App is instantiated for each Account individually and is migratable. Apps are allowed to perform actions on Abstract Accounts and can integrate with other Apps and Adapters installed on the Account. To learn more about Abstract Accounts, please see the [abstract accounts documentation](https://docs.abstract.money/3_framework/3_architecture.html). To read more about apps, please see the [app module documentation](https://docs.abstract.money/3_framework/6_module_types.html). + +## Getting Started + +### Requirements + +Learn more about the requirements for developing Abstract apps in the [getting started documentation](https://docs.abstract.money/4_get_started/1_index.html). + +### Setup + +To get started, clone this repository and run the following command: + +```shell +chmod +x ./template-setup.sh +./template-setup.sh +``` + +The setup will suggest you to install a few tools that are used in the template. You can skip this step if you already have them installed or if you're not planning on using them. + +## Using the Justfile + +This repository comes with a [`justfile`](https://github.com/casey/just), which is a handy task runner that helps with building, testing, and publishing your Abstract app module. + +### Installing Tools + +To fully make use of the `justfile`, you need to install a few tools first. You can do this by simply running `just install-tools`. See [tools used the template](https://docs.abstract.money/3_get_started/2_installation.html?#tools-used-in-the-template) for more information. + +### Available Tasks + +Here are some of the tasks available in the `justfile`: + +- `install-tools`: Install all the tools needed to run the tasks. +- `build`: Build everything with all features. +- `test`: Run all tests. +- `watch-test`: Watch the codebase and run tests on changes. +- `fmt`: Format the codebase (including .toml). +- `lint`: Lint-check the codebase. +- `lintfix`: Fix linting errors automatically. +- `watch`: Watch the codebase and run `cargo check` on changes. +- `check`: Check the codebase for issues. +- `publish {{chain-id}}`: Publish the App to a network. +- `wasm`: Optimize the contract. +- `schema`: Generate the json schemas for the contract + + +- `publish-schemas`: Publish the schemas by creating a PR on the Abstract [schemas](https://github.com/AbstractSDK/schemas) repository. + +You can see the full list of tasks available by running `just --list`. + +### Compiling + +Best to run `cargo update` to have synced versions just in case. + +You can compile your module by running the following command: +```sh +just wasm +``` +This should result in an artifacts directory being created in your project root. Inside you will find a `my_module.wasm` file that is your module’s binary. + +### Testing + +You can test the module using the different provided methods. + +1. **Integration testing:** We provide an integration testing setup [here](./tests/integration.rs). You should use this to set up your environment and test the different execution and query entry-points of your module. Once you are satisfied with the results you can try publishing it to a real chain. +2. **Local Daemon:** Once you have confirmed that your module works as expected you can spin up a local node and deploy Abstract + your app onto the chain. You need [Docker](https://www.docker.com/) installed for this step. You can do this by running the [test-local](./examples/test-local.rs) example, which uses a locally running juno daemon to deploy to. You can setup local juno using `just juno-local` command. At this point you can also test your front-end with the contracts. + +Once testing is done you can attempt an actual deployment on test and mainnet. + +### Publishing + +Before attempting to publish your app you need to add your mnemonic to the `.env` file. **Don't use a mnemonic that has mainnet funds for this.** + + +Select from a wide range of [supported chains](https://orchestrator.abstract.money/chains/index.html) before proceeding. Make sure you've some balance enough to pay gas for the transaction. If the chain does not have gas, complete at least 1 transaction with your account before proceeding. + +You can now use `just publish {{chain-id}}` to run the [`examples/publish.rs`](./examples/publish.rs) script. The script will publish the app to the networks that you provided. Make sure you have enough funds in your wallet on the different networks you aim to publish on. + +### Publishing Module Schemas + +To publish your module schemas, we provide the `publish-schemas` command, which creates a pull request on the Abstract [schemas](https://github.com/AbstractSDK/schemas) repository. + +Please install [github cli](https://cli.github.com/) before proceeding. Also login and setup your github auth by `gh auth login`. Now, we're ready to proceed. + +```bash +just publish-schemas +``` + +- `namespace`: Your module's namespace +- `name`: Your module's name +- `version`: Your module's version. Note that if you only include the minor version (e.g., `0.1`), you don't have to reupload the schemas for every patch version. + +The command will automatically clone the Abstract Schemas repository, create a new branch with the given namespace, name, and version, and copy the schemas and metadata from your module to the appropriate directory. + +For this command to work properly, please make sure that your `metadata.json` file is located at the root of your module's directory. This file is necessary for the Abstract Frontend to correctly interpret and display information about your module. + +Example: + +```bash +just publish-schemas my-namespace my-module 0.0.1 +``` + +In the example above, `my-namespace` is the namespace, `my-module` is the module's name, and `0.1` is the minor version. If you create a patch for your module (e.g., `0.1.1`), you don't need to run `publish-schemas` again unless the schemas have changed. + +## Contributing + +We welcome contributions to the Abstract App Module Template! To contribute, fork this repository and submit a pull request with your changes. If you have any questions or issues, please open an issue in the repository and we will be happy to assist you. + +## Community +Check out the following places for support, discussions & feedback: + +- Join our [Discord server](https://discord.com/invite/uch3Tq3aym) diff --git a/example.env b/example.env new file mode 100644 index 0000000..1c15398 --- /dev/null +++ b/example.env @@ -0,0 +1,8 @@ +RUST_LOG="info" + +LOCAL_MNEMONIC="" +TEST_MNEMONIC="" +MAIN_MNEMONIC="" + +# See all available env variables: +# https://orchestrator.abstract.money/contracts/env-variable.html \ No newline at end of file diff --git a/examples/local_daemon.rs b/examples/local_daemon.rs new file mode 100644 index 0000000..b003fc2 --- /dev/null +++ b/examples/local_daemon.rs @@ -0,0 +1,75 @@ +//! Deploys Abstract and the App module to a local Junod instance. See how to spin up a local chain here: https://docs.junonetwork.io/developer-guides/junod-local-dev-setup +//! You can also start a juno container by running `just juno-local`. +//! +//! Ensure the local juno is running before executing this script. +//! Also make sure port 9090 is exposed on the local juno container. This port is used to communicate with the chain. +//! +//! # Run +//! +//! `cargo run --example local_daemon` + +use abstract_app::objects::namespace::Namespace; +use abstract_client::{AbstractClient, Publisher}; +use app::{ + contract::{APP_ID, APP_VERSION}, + msg::AppInstantiateMsg, + AppInterface, +}; +use cw_orch::{anyhow, prelude::*, tokio::runtime::Runtime}; +use semver::Version; +use speculoos::assert_that; + +const LOCAL_MNEMONIC: &str = "clip hire initial neck maid actor venue client foam budget lock catalog sweet steak waste crater broccoli pipe steak sister coyote moment obvious choose"; + +fn main() -> anyhow::Result<()> { + dotenv::dotenv().ok(); + env_logger::init(); + + let _version: Version = APP_VERSION.parse().unwrap(); + let runtime = Runtime::new()?; + + let daemon = Daemon::builder() + .chain(networks::LOCAL_JUNO) + .mnemonic(LOCAL_MNEMONIC) + .handle(runtime.handle()) + .build() + .unwrap(); + + let app_namespace = Namespace::from_id(APP_ID)?; + + // Create an [`AbstractClient`] + let abstract_client: AbstractClient = AbstractClient::new(daemon.clone())?; + + // Get the [`Publisher`] that owns the namespace. + // If there isn't one, it creates an Account and claims the namespace. + let publisher: Publisher<_> = abstract_client.publisher_builder(app_namespace).build()?; + + // Ensure the current sender owns the namespace + if publisher.account().owner()? != daemon.sender() { + panic!("The current sender can not publish to this namespace. Please use the wallet that owns the Account that owns the Namespace.") + } + + // Publish the App to the Abstract Platform + publisher.publish_app::>()?; + + // Install the App on a new account + + let account = abstract_client.account_builder().build()?; + // Installs the app on the Account + let app = account.install_app::>(&AppInstantiateMsg { count: 0 }, &[])?; + + // Import app's endpoint function traits for easy interactions. + use app::{AppExecuteMsgFns, AppQueryMsgFns}; + assert_that!(app.count()?.count).is_equal_to(0); + + // Execute the App + app.increment()?; + + // Query the App again + assert_that!(app.count()?.count).is_equal_to(1); + + // Note: the App is installed on a sub-account of the main account! + assert_ne!(account.id()?, app.account().id()?); + + Ok(()) +} diff --git a/examples/publish.rs b/examples/publish.rs new file mode 100644 index 0000000..6e91644 --- /dev/null +++ b/examples/publish.rs @@ -0,0 +1,68 @@ +//! Publishes the module to the Abstract platform by uploading it and registering it on the app store. +//! +//! Info: The mnemonic used to register the module must be the same as the owner of the account that claimed the namespace. +//! +//! ## Example +//! +//! ```bash +//! $ just publish uni-6 osmo-test-5 +//! ``` +use abstract_app::objects::namespace::Namespace; +use abstract_client::{AbstractClient, Publisher}; +use app::{contract::APP_ID, AppInterface}; +use clap::Parser; +use cw_orch::{ + anyhow, + daemon::{ChainInfo, Daemon}, + environment::TxHandler, + prelude::{networks::parse_network, DaemonBuilder}, + tokio::runtime::Runtime, +}; + +fn publish(networks: Vec) -> anyhow::Result<()> { + // run for each requested network + for network in networks { + // Setup + let rt = Runtime::new()?; + let chain = DaemonBuilder::default() + .handle(rt.handle()) + .chain(network) + .build()?; + + let app_namespace = Namespace::from_id(APP_ID)?; + + // Create an [`AbstractClient`] + let abstract_client: AbstractClient = AbstractClient::new(chain.clone())?; + + // Get the [`Publisher`] that owns the namespace, otherwise create a new one and claim the namespace + let publisher: Publisher<_> = abstract_client.publisher_builder(app_namespace).build()?; + + if publisher.account().owner()? != chain.sender() { + panic!("The current sender can not publish to this namespace. Please use the wallet that owns the Account that owns the Namespace.") + } + + // Publish the App to the Abstract Platform + publisher.publish_app::>()?; + } + Ok(()) +} + +#[derive(Parser, Default, Debug)] +#[command(author, version, about, long_about = None)] +struct Arguments { + /// Network Id to publish on + #[arg(short, long, value_delimiter = ' ', num_args = 1..)] + network_ids: Vec, +} + +fn main() { + dotenv::dotenv().ok(); + env_logger::init(); + let args = Arguments::parse(); + let networks = args + .network_ids + .iter() + .map(|n| parse_network(n).unwrap()) + .collect(); + publish(networks).unwrap(); +} diff --git a/examples/schema.rs b/examples/schema.rs new file mode 100644 index 0000000..86c5c8e --- /dev/null +++ b/examples/schema.rs @@ -0,0 +1,14 @@ +use app::contract::App; +use cosmwasm_schema::remove_schemas; +use std::env::current_dir; +use std::fs::create_dir_all; + +fn main() { + let mut out_dir = current_dir().unwrap(); + out_dir.push("schema"); + create_dir_all(&out_dir).unwrap(); + remove_schemas(&out_dir).unwrap(); + + #[cfg(feature = "schema")] + App::export_schema(&out_dir); +} diff --git a/justfile b/justfile new file mode 100644 index 0000000..73c7d6c --- /dev/null +++ b/justfile @@ -0,0 +1,132 @@ +# Install the tools that are used in this justfile +install-tools: + cargo install cargo-nextest --locked || true + cargo install taplo-cli --locked || true + cargo install cargo-watch || true + cargo install cargo-limit || true + +## Development Helpers ## + +# Build everything +build: + cargo build --all-features + +# Test everything +test: + cargo nextest run + +watch-test: + cargo watch -x "nextest run" + +# Format your code and `Cargo.toml` files +fmt: + cargo fmt --all + find . -type f -iname "*.toml" -print0 | xargs -0 taplo format + +lint: + cargo clippy --all -- -D warnings + +lintfix: + cargo clippy --fix --allow-staged --allow-dirty --all-features + just fmt + +watch: + cargo watch -x "lcheck --all-features" + +check: + cargo check --all-features + +juno-local: + docker kill juno_node_1 || true + docker volume rm -f junod_data || true + docker run --rm -d \ + --name juno_node_1 \ + -p 1317:1317 \ + -p 26656:26656 \ + -p 26657:26657 \ + -p 9090:9090 \ + -e STAKE_TOKEN=ujunox \ + -e UNSAFE_CORS=true \ + --mount type=volume,source=junod_data,target=/root \ + ghcr.io/cosmoscontracts/juno:15.0.0 \ + ./setup_and_run.sh juno16g2rahf5846rxzp3fwlswy08fz8ccuwk03k57y # You can add used sender addresses here + +wasm: + #!/usr/bin/env bash + + # Delete all the current wasms first + rm -rf ./artifacts/*.wasm + + if [[ $(arch) == "arm64" ]]; then + image="cosmwasm/rust-optimizer-arm64" + else + image="cosmwasm/rust-optimizer" + fi + + # Optimized builds + docker run --rm -v "$(pwd)":/code \ + --mount type=volume,source="$(basename "$(pwd)")_cache",target=/code/target \ + --mount type=volume,source=registry_cache,target=/usr/local/cargo/registry \ + ${image}:0.14.0 + +# Generate the schemas for the app contract +schema: + cargo schema + +# Generate the schemas for this app and publish them to the schemas repository for access in the Abstract frontend +publish-schemas namespace name version: schema + #!/usr/bin/env bash + set -euxo pipefail + + # Pre-run check for 'gh' CLI tool + if ! command -v gh &> /dev/null; then \ + echo "'gh' could not be found. Please install GitHub CLI."; exit; \ + fi + + # check that the metadata exists + if [ ! -e "./metadata.json" ]; then \ + echo "Please create metadata.json for module metadata"; exit; \ + fi + + tmp_dir="$(mktemp -d)" + schema_out_dir="$tmp_dir/{{namespace}}/{{name}}/{{version}}" + metadata_out_dir="$tmp_dir/{{namespace}}/{{name}}" + + # Clone the repository to the temporary directory + git clone https://github.com/AbstractSDK/schemas "$tmp_dir" + + # Create target directory structure and copy schemas + mkdir -p "$schema_out_dir" + cp -a "./schema/." "$schema_out_dir" + + # Copy metadata.json to the target directory + cp "./metadata.json" "$metadata_out_dir" + + # Create a new branch with a name based on the inputs + cd "$tmp_dir" + git checkout -b '{{namespace}}/{{name}}/{{version}}' + + # Stage all new and changed files for commit + git add . + + # Commit the changes with a message + git commit -m 'Add schemas for {{namespace}} {{name}} {{version}}' + + # Create a pull request using 'gh' CLI tool + gh pr create --title 'Add schemas for {{namespace}} {{name}} {{version}}' --body "" + +## Exection commands ## + +run-script script +CHAINS: + cargo run --example {{script}} --features="daemon" -- --network-ids {{CHAINS}} + +publish +CHAINS: + #!/usr/bin/env bash + set -euxo pipefail + + if [ -d "artifacts" ]; then + echo "Build found ✅"; + else + just wasm + fi + just run-script publish {{CHAINS}} diff --git a/metadata.json b/metadata.json new file mode 100644 index 0000000..338488b --- /dev/null +++ b/metadata.json @@ -0,0 +1,9 @@ +{ + "name": "App", + "description": "App description.", + "website": "", + "docs": "", + "type": "api", + "icon": "GiTrade", + "enabled": true +} diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..40e71ad --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,4 @@ +# stable +newline_style = "Unix" +hard_tabs = false +tab_spaces = 4 diff --git a/schema/execute_msg.json b/schema/execute_msg.json new file mode 100644 index 0000000..7b63cdf --- /dev/null +++ b/schema/execute_msg.json @@ -0,0 +1,393 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ExecuteMsg", + "description": "Wrapper around all possible messages that can be sent to the module.", + "oneOf": [ + { + "description": "A configuration message, defined by the base.", + "type": "object", + "required": [ + "base" + ], + "properties": { + "base": { + "$ref": "#/definitions/BaseExecuteMsg" + } + }, + "additionalProperties": false + }, + { + "description": "An app request defined by a base consumer.", + "type": "object", + "required": [ + "module" + ], + "properties": { + "module": { + "$ref": "#/definitions/AppExecuteMsg" + } + }, + "additionalProperties": false + }, + { + "description": "IbcReceive to process IBC callbacks In order to trust this, the apps and adapters verify this comes from the ibc-client contract.", + "type": "object", + "required": [ + "ibc_callback" + ], + "properties": { + "ibc_callback": { + "$ref": "#/definitions/IbcResponseMsg" + } + }, + "additionalProperties": false + }, + { + "description": "Receive endpoint for CW20 / external service integrations", + "type": "object", + "required": [ + "receive" + ], + "properties": { + "receive": { + "$ref": "#/definitions/Empty" + } + }, + "additionalProperties": false + } + ], + "definitions": { + "AppExecuteMsg": { + "description": "App execute messages", + "oneOf": [ + { + "description": "Increment count by 1", + "type": "object", + "required": [ + "increment" + ], + "properties": { + "increment": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Admin method - reset count", + "type": "object", + "required": [ + "reset" + ], + "properties": { + "reset": { + "type": "object", + "required": [ + "count" + ], + "properties": { + "count": { + "description": "Count value after reset", + "type": "integer", + "format": "int32" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "update_config" + ], + "properties": { + "update_config": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "Attribute": { + "description": "An key value pair that is used in the context of event attributes in logs", + "type": "object", + "required": [ + "key", + "value" + ], + "properties": { + "key": { + "type": "string" + }, + "value": { + "type": "string" + } + } + }, + "BaseExecuteMsg": { + "oneOf": [ + { + "description": "Updates the base config", + "type": "object", + "required": [ + "update_config" + ], + "properties": { + "update_config": { + "type": "object", + "properties": { + "ans_host_address": { + "type": [ + "string", + "null" + ] + }, + "version_control_address": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "Binary": { + "description": "Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec. See also .", + "type": "string" + }, + "Callback": { + "oneOf": [ + { + "description": "Result of executing the requested query, or an error.\n\nresult[i] corresponds to the i'th query and contains the base64 encoded query response.", + "type": "object", + "required": [ + "query" + ], + "properties": { + "query": { + "$ref": "#/definitions/Result_of_Array_of_Binary_or_ErrorResponse" + } + }, + "additionalProperties": false + }, + { + "description": "Result of executing the requested messages, or an error.\n\n14/04/23: if a submessage errors the reply handler can see `codespace: wasm, code: 5`, but not the actual error. as a result, we can't return good errors for Execution and this error string will only tell you the error's codespace. for example, an out-of-gas error is code 11 and looks like `codespace: sdk, code: 11`.", + "type": "object", + "required": [ + "execute" + ], + "properties": { + "execute": { + "$ref": "#/definitions/Result_of_ExecutionResponse_or_String" + } + }, + "additionalProperties": false + }, + { + "description": "An error occured that could not be recovered from. The only known way that this can occur is message handling running out of gas, in which case the error will be `codespace: sdk, code: 11`.\n\nThis error is not named becuase it could also occur due to a panic or unhandled error during message processing. We don't expect this to happen and have carefully written the code to avoid it.", + "type": "object", + "required": [ + "fatal_error" + ], + "properties": { + "fatal_error": { + "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" + }, + "ErrorResponse": { + "type": "object", + "required": [ + "error", + "message_index" + ], + "properties": { + "error": { + "description": "The error that occured executing the message.", + "type": "string" + }, + "message_index": { + "description": "The index of the first message who's execution failed.", + "allOf": [ + { + "$ref": "#/definitions/Uint64" + } + ] + } + }, + "additionalProperties": false + }, + "Event": { + "description": "A full [*Cosmos SDK* event].\n\nThis version uses string attributes (similar to [*Cosmos SDK* StringEvent]), which then get magically converted to bytes for Tendermint somewhere between the Rust-Go interface, JSON deserialization and the `NewEvent` call in Cosmos SDK.\n\n[*Cosmos SDK* event]: https://docs.cosmos.network/main/learn/advanced/events [*Cosmos SDK* StringEvent]: https://github.com/cosmos/cosmos-sdk/blob/v0.42.5/proto/cosmos/base/abci/v1beta1/abci.proto#L56-L70", + "type": "object", + "required": [ + "attributes", + "type" + ], + "properties": { + "attributes": { + "description": "The attributes to be included in the event.\n\nYou can learn more about these from [*Cosmos SDK* docs].\n\n[*Cosmos SDK* docs]: https://docs.cosmos.network/main/learn/advanced/events", + "type": "array", + "items": { + "$ref": "#/definitions/Attribute" + } + }, + "type": { + "description": "The event type. This is renamed to \"ty\" because \"type\" is reserved in Rust. This sucks, we know.", + "type": "string" + } + } + }, + "ExecutionResponse": { + "type": "object", + "required": [ + "executed_by", + "result" + ], + "properties": { + "executed_by": { + "description": "The address on the remote chain that executed the messages.", + "type": "string" + }, + "result": { + "description": "Index `i` corresponds to the result of executing the `i`th message.", + "type": "array", + "items": { + "$ref": "#/definitions/SubMsgResponse" + } + } + }, + "additionalProperties": false + }, + "IbcResponseMsg": { + "description": "IbcResponseMsg should be de/serialized under `IbcCallback()` variant in a ExecuteMsg", + "type": "object", + "required": [ + "id", + "result" + ], + "properties": { + "id": { + "description": "The ID chosen by the caller in the `callback_info.id`", + "type": "string" + }, + "msg": { + "description": "The msg sent with the callback request. This is usually used to provide information to the ibc callback function for context", + "anyOf": [ + { + "$ref": "#/definitions/Binary" + }, + { + "type": "null" + } + ] + }, + "result": { + "$ref": "#/definitions/Callback" + } + }, + "additionalProperties": false + }, + "Result_of_Array_of_Binary_or_ErrorResponse": { + "oneOf": [ + { + "type": "object", + "required": [ + "Ok" + ], + "properties": { + "Ok": { + "type": "array", + "items": { + "$ref": "#/definitions/Binary" + } + } + } + }, + { + "type": "object", + "required": [ + "Err" + ], + "properties": { + "Err": { + "$ref": "#/definitions/ErrorResponse" + } + } + } + ] + }, + "Result_of_ExecutionResponse_or_String": { + "oneOf": [ + { + "type": "object", + "required": [ + "Ok" + ], + "properties": { + "Ok": { + "$ref": "#/definitions/ExecutionResponse" + } + } + }, + { + "type": "object", + "required": [ + "Err" + ], + "properties": { + "Err": { + "type": "string" + } + } + } + ] + }, + "SubMsgResponse": { + "description": "The information we get back from a successful sub message execution, with full Cosmos SDK events.", + "type": "object", + "required": [ + "events" + ], + "properties": { + "data": { + "anyOf": [ + { + "$ref": "#/definitions/Binary" + }, + { + "type": "null" + } + ] + }, + "events": { + "type": "array", + "items": { + "$ref": "#/definitions/Event" + } + } + } + }, + "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/schema/instantiate_msg.json b/schema/instantiate_msg.json new file mode 100644 index 0000000..278695c --- /dev/null +++ b/schema/instantiate_msg.json @@ -0,0 +1,87 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "InstantiateMsg", + "type": "object", + "required": [ + "base", + "module" + ], + "properties": { + "base": { + "description": "base instantiate information", + "allOf": [ + { + "$ref": "#/definitions/BaseInstantiateMsg" + } + ] + }, + "module": { + "description": "custom instantiate msg", + "allOf": [ + { + "$ref": "#/definitions/AppInstantiateMsg" + } + ] + } + }, + "additionalProperties": false, + "definitions": { + "AccountBase": { + "description": "Contains the minimal Abstract Account contract addresses.", + "type": "object", + "required": [ + "manager", + "proxy" + ], + "properties": { + "manager": { + "$ref": "#/definitions/Addr" + }, + "proxy": { + "$ref": "#/definitions/Addr" + } + }, + "additionalProperties": false + }, + "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" + }, + "AppInstantiateMsg": { + "description": "App instantiate message", + "type": "object", + "required": [ + "count" + ], + "properties": { + "count": { + "description": "Initial count", + "type": "integer", + "format": "int32" + } + }, + "additionalProperties": false + }, + "BaseInstantiateMsg": { + "description": "Used by Module Factory to instantiate App", + "type": "object", + "required": [ + "account_base", + "ans_host_address", + "version_control_address" + ], + "properties": { + "account_base": { + "$ref": "#/definitions/AccountBase" + }, + "ans_host_address": { + "type": "string" + }, + "version_control_address": { + "type": "string" + } + }, + "additionalProperties": false + } + } +} diff --git a/schema/migrate_msg.json b/schema/migrate_msg.json new file mode 100644 index 0000000..5ebb337 --- /dev/null +++ b/schema/migrate_msg.json @@ -0,0 +1,38 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "MigrateMsg", + "type": "object", + "required": [ + "base", + "module" + ], + "properties": { + "base": { + "description": "base migrate information", + "allOf": [ + { + "$ref": "#/definitions/BaseMigrateMsg" + } + ] + }, + "module": { + "description": "custom migrate msg", + "allOf": [ + { + "$ref": "#/definitions/AppMigrateMsg" + } + ] + } + }, + "additionalProperties": false, + "definitions": { + "AppMigrateMsg": { + "type": "object", + "additionalProperties": false + }, + "BaseMigrateMsg": { + "type": "object", + "additionalProperties": false + } + } +} diff --git a/schema/module-schema.json b/schema/module-schema.json new file mode 100644 index 0000000..6d5b1d9 --- /dev/null +++ b/schema/module-schema.json @@ -0,0 +1,143 @@ +{ + "contract_name": "module-schema", + "contract_version": "0.21.0", + "idl_version": "1.0.0", + "instantiate": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "InstantiateMsg", + "description": "App instantiate message", + "type": "object", + "required": [ + "count" + ], + "properties": { + "count": { + "description": "Initial count", + "type": "integer", + "format": "int32" + } + }, + "additionalProperties": false + }, + "execute": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ExecuteMsg", + "description": "App execute messages", + "oneOf": [ + { + "description": "Increment count by 1", + "type": "object", + "required": [ + "increment" + ], + "properties": { + "increment": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Admin method - reset count", + "type": "object", + "required": [ + "reset" + ], + "properties": { + "reset": { + "type": "object", + "required": [ + "count" + ], + "properties": { + "count": { + "description": "Count value after reset", + "type": "integer", + "format": "int32" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "update_config" + ], + "properties": { + "update_config": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "query": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "QueryMsg", + "description": "App query messages", + "oneOf": [ + { + "type": "object", + "required": [ + "config" + ], + "properties": { + "config": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "count" + ], + "properties": { + "count": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "migrate": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "MigrateMsg", + "type": "object", + "additionalProperties": false + }, + "sudo": null, + "responses": { + "config": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ConfigResponse", + "type": "object", + "additionalProperties": false + }, + "count": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "CountResponse", + "type": "object", + "required": [ + "count" + ], + "properties": { + "count": { + "type": "integer", + "format": "int32" + } + }, + "additionalProperties": false + } + } +} diff --git a/schema/query_msg.json b/schema/query_msg.json new file mode 100644 index 0000000..e244d6a --- /dev/null +++ b/schema/query_msg.json @@ -0,0 +1,125 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "QueryMsg", + "oneOf": [ + { + "description": "A query to the base.", + "type": "object", + "required": [ + "base" + ], + "properties": { + "base": { + "$ref": "#/definitions/BaseQueryMsg" + } + }, + "additionalProperties": false + }, + { + "description": "Custom query", + "type": "object", + "required": [ + "module" + ], + "properties": { + "module": { + "$ref": "#/definitions/AppQueryMsg" + } + }, + "additionalProperties": false + } + ], + "definitions": { + "AppQueryMsg": { + "description": "App query messages", + "oneOf": [ + { + "type": "object", + "required": [ + "config" + ], + "properties": { + "config": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "count" + ], + "properties": { + "count": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "BaseQueryMsg": { + "oneOf": [ + { + "description": "Returns [`AppConfigResponse`]", + "type": "object", + "required": [ + "base_config" + ], + "properties": { + "base_config": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns the admin. Returns [`AdminResponse`]", + "type": "object", + "required": [ + "base_admin" + ], + "properties": { + "base_admin": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns module data Returns [`ModuleDataResponse`]", + "type": "object", + "required": [ + "module_data" + ], + "properties": { + "module_data": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns top level owner Returns [`TopLevelOwnerResponse`]", + "type": "object", + "required": [ + "top_level_owner" + ], + "properties": { + "top_level_owner": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + } + } +} diff --git a/schema/raw/execute.json b/schema/raw/execute.json new file mode 100644 index 0000000..fe6f045 --- /dev/null +++ b/schema/raw/execute.json @@ -0,0 +1,58 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ExecuteMsg", + "description": "App execute messages", + "oneOf": [ + { + "description": "Increment count by 1", + "type": "object", + "required": [ + "increment" + ], + "properties": { + "increment": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Admin method - reset count", + "type": "object", + "required": [ + "reset" + ], + "properties": { + "reset": { + "type": "object", + "required": [ + "count" + ], + "properties": { + "count": { + "description": "Count value after reset", + "type": "integer", + "format": "int32" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "update_config" + ], + "properties": { + "update_config": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] +} diff --git a/schema/raw/instantiate.json b/schema/raw/instantiate.json new file mode 100644 index 0000000..994e961 --- /dev/null +++ b/schema/raw/instantiate.json @@ -0,0 +1,17 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "InstantiateMsg", + "description": "App instantiate message", + "type": "object", + "required": [ + "count" + ], + "properties": { + "count": { + "description": "Initial count", + "type": "integer", + "format": "int32" + } + }, + "additionalProperties": false +} diff --git a/schema/raw/migrate.json b/schema/raw/migrate.json new file mode 100644 index 0000000..7fbe8c5 --- /dev/null +++ b/schema/raw/migrate.json @@ -0,0 +1,6 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "MigrateMsg", + "type": "object", + "additionalProperties": false +} diff --git a/schema/raw/query.json b/schema/raw/query.json new file mode 100644 index 0000000..62628be --- /dev/null +++ b/schema/raw/query.json @@ -0,0 +1,33 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "QueryMsg", + "description": "App query messages", + "oneOf": [ + { + "type": "object", + "required": [ + "config" + ], + "properties": { + "config": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "count" + ], + "properties": { + "count": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] +} diff --git a/schema/raw/response_to_base_admin.json b/schema/raw/response_to_base_admin.json new file mode 100644 index 0000000..c73969a --- /dev/null +++ b/schema/raw/response_to_base_admin.json @@ -0,0 +1,15 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "AdminResponse", + "description": "Returned from Admin.query_admin()", + "type": "object", + "properties": { + "admin": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false +} diff --git a/schema/raw/response_to_base_config.json b/schema/raw/response_to_base_config.json new file mode 100644 index 0000000..0f29e90 --- /dev/null +++ b/schema/raw/response_to_base_config.json @@ -0,0 +1,28 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "AppConfigResponse", + "type": "object", + "required": [ + "ans_host_address", + "manager_address", + "proxy_address" + ], + "properties": { + "ans_host_address": { + "$ref": "#/definitions/Addr" + }, + "manager_address": { + "$ref": "#/definitions/Addr" + }, + "proxy_address": { + "$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" + } + } +} diff --git a/schema/raw/response_to_config.json b/schema/raw/response_to_config.json new file mode 100644 index 0000000..487b0f2 --- /dev/null +++ b/schema/raw/response_to_config.json @@ -0,0 +1,6 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ConfigResponse", + "type": "object", + "additionalProperties": false +} diff --git a/schema/raw/response_to_count.json b/schema/raw/response_to_count.json new file mode 100644 index 0000000..e9c5891 --- /dev/null +++ b/schema/raw/response_to_count.json @@ -0,0 +1,15 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "CountResponse", + "type": "object", + "required": [ + "count" + ], + "properties": { + "count": { + "type": "integer", + "format": "int32" + } + }, + "additionalProperties": false +} diff --git a/schema/raw/response_to_module_data.json b/schema/raw/response_to_module_data.json new file mode 100644 index 0000000..9932c0d --- /dev/null +++ b/schema/raw/response_to_module_data.json @@ -0,0 +1,52 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ModuleDataResponse", + "type": "object", + "required": [ + "dependencies", + "module_id", + "version" + ], + "properties": { + "dependencies": { + "type": "array", + "items": { + "$ref": "#/definitions/DependencyResponse" + } + }, + "metadata": { + "type": [ + "string", + "null" + ] + }, + "module_id": { + "type": "string" + }, + "version": { + "type": "string" + } + }, + "additionalProperties": false, + "definitions": { + "DependencyResponse": { + "type": "object", + "required": [ + "id", + "version_req" + ], + "properties": { + "id": { + "type": "string" + }, + "version_req": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + } + } +} diff --git a/schema/raw/response_to_top_level_owner.json b/schema/raw/response_to_top_level_owner.json new file mode 100644 index 0000000..13fc072 --- /dev/null +++ b/schema/raw/response_to_top_level_owner.json @@ -0,0 +1,20 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "TopLevelOwnerResponse", + "type": "object", + "required": [ + "address" + ], + "properties": { + "address": { + "$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" + } + } +} diff --git a/src/contract.rs b/src/contract.rs new file mode 100644 index 0000000..997075e --- /dev/null +++ b/src/contract.rs @@ -0,0 +1,34 @@ +use crate::msg::AppMigrateMsg; +use crate::{ + error::AppError, + handlers, + msg::{AppExecuteMsg, AppInstantiateMsg, AppQueryMsg}, + replies::{self, INSTANTIATE_REPLY_ID}, +}; +use abstract_app::AppContract; +use cosmwasm_std::Response; + +/// The version of your app +pub const APP_VERSION: &str = env!("CARGO_PKG_VERSION"); +/// The id of the app +pub const APP_ID: &str = "my-namespace:app"; + +/// The type of the result returned by your app's entry points. +pub type AppResult = Result; + +/// The type of the app that is used to build your app and access the Abstract SDK features. +pub type App = AppContract; + +const APP: App = App::new(APP_ID, APP_VERSION, None) + .with_instantiate(handlers::instantiate_handler) + .with_execute(handlers::execute_handler) + .with_query(handlers::query_handler) + .with_migrate(handlers::migrate_handler) + .with_replies(&[(INSTANTIATE_REPLY_ID, replies::instantiate_reply)]); + +// Export handlers +#[cfg(feature = "export")] +abstract_app::export_endpoints!(APP, App); + +#[cfg(feature = "interface")] +abstract_app::cw_orch_interface!(APP, App, AppInterface); diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..44ecbc5 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,28 @@ +use abstract_app::abstract_core::AbstractError; +use abstract_app::abstract_sdk::AbstractSdkError; +use abstract_app::AppError as AbstractAppError; +use cosmwasm_std::StdError; +use cw_asset::AssetError; +use cw_controllers::AdminError; +use thiserror::Error; + +#[derive(Error, Debug, PartialEq)] +pub enum AppError { + #[error("{0}")] + Std(#[from] StdError), + + #[error("{0}")] + Abstract(#[from] AbstractError), + + #[error("{0}")] + AbstractSdk(#[from] AbstractSdkError), + + #[error("{0}")] + Asset(#[from] AssetError), + + #[error("{0}")] + Admin(#[from] AdminError), + + #[error("{0}")] + DappError(#[from] AbstractAppError), +} diff --git a/src/handlers/execute.rs b/src/handlers/execute.rs new file mode 100644 index 0000000..0d1ab3d --- /dev/null +++ b/src/handlers/execute.rs @@ -0,0 +1,43 @@ +use abstract_app::traits::AbstractResponse; +use cosmwasm_std::{DepsMut, Env, MessageInfo}; + +use crate::contract::{App, AppResult}; + +use crate::msg::AppExecuteMsg; +use crate::state::{CONFIG, COUNT}; + +pub fn execute_handler( + deps: DepsMut, + _env: Env, + info: MessageInfo, + app: App, + msg: AppExecuteMsg, +) -> AppResult { + match msg { + AppExecuteMsg::Increment {} => increment(deps, app), + AppExecuteMsg::Reset { count } => reset(deps, info, count, app), + AppExecuteMsg::UpdateConfig {} => update_config(deps, info, app), + } +} + +fn increment(deps: DepsMut, app: App) -> AppResult { + COUNT.update(deps.storage, |count| AppResult::Ok(count + 1))?; + + Ok(app.response("increment")) +} + +fn reset(deps: DepsMut, info: MessageInfo, count: i32, app: App) -> AppResult { + app.admin.assert_admin(deps.as_ref(), &info.sender)?; + COUNT.save(deps.storage, &count)?; + + Ok(app.response("reset")) +} + +/// Update the configuration of the app +fn update_config(deps: DepsMut, msg_info: MessageInfo, app: App) -> AppResult { + // Only the admin should be able to call this + app.admin.assert_admin(deps.as_ref(), &msg_info.sender)?; + let mut _config = CONFIG.load(deps.storage)?; + + Ok(app.response("update_config")) +} diff --git a/src/handlers/instantiate.rs b/src/handlers/instantiate.rs new file mode 100644 index 0000000..070a27f --- /dev/null +++ b/src/handlers/instantiate.rs @@ -0,0 +1,21 @@ +use cosmwasm_std::{DepsMut, Env, MessageInfo, Response}; + +use crate::contract::{App, AppResult}; +use crate::msg::AppInstantiateMsg; +use crate::state::{Config, CONFIG, COUNT}; + +pub fn instantiate_handler( + deps: DepsMut, + _env: Env, + _info: MessageInfo, + _app: App, + msg: AppInstantiateMsg, +) -> AppResult { + let config: Config = Config {}; + + CONFIG.save(deps.storage, &config)?; + COUNT.save(deps.storage, &msg.count)?; + + // Example instantiation that doesn't do anything + Ok(Response::new()) +} diff --git a/src/handlers/migrate.rs b/src/handlers/migrate.rs new file mode 100644 index 0000000..cf12abd --- /dev/null +++ b/src/handlers/migrate.rs @@ -0,0 +1,10 @@ +use crate::contract::{App, AppResult}; +use crate::msg::AppMigrateMsg; +use abstract_app::traits::AbstractResponse; +use cosmwasm_std::{DepsMut, Env}; + +/// Handle the app migrate msg +/// The top-level Abstract app does version checking and dispatches to this handler +pub fn migrate_handler(_deps: DepsMut, _env: Env, app: App, _msg: AppMigrateMsg) -> AppResult { + Ok(app.response("migrate")) +} diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs new file mode 100644 index 0000000..bd14bb0 --- /dev/null +++ b/src/handlers/mod.rs @@ -0,0 +1,9 @@ +pub mod execute; +pub mod instantiate; +pub mod migrate; +pub mod query; + +pub use crate::handlers::{ + execute::execute_handler, instantiate::instantiate_handler, migrate::migrate_handler, + query::query_handler, +}; diff --git a/src/handlers/query.rs b/src/handlers/query.rs new file mode 100644 index 0000000..3abe555 --- /dev/null +++ b/src/handlers/query.rs @@ -0,0 +1,22 @@ +use crate::contract::{App, AppResult}; +use crate::msg::{AppQueryMsg, ConfigResponse, CountResponse}; +use crate::state::{CONFIG, COUNT}; +use cosmwasm_std::{to_json_binary, Binary, Deps, Env, StdResult}; + +pub fn query_handler(deps: Deps, _env: Env, _app: &App, msg: AppQueryMsg) -> AppResult { + match msg { + AppQueryMsg::Config {} => to_json_binary(&query_config(deps)?), + AppQueryMsg::Count {} => to_json_binary(&query_count(deps)?), + } + .map_err(Into::into) +} + +fn query_config(deps: Deps) -> StdResult { + let _config = CONFIG.load(deps.storage)?; + Ok(ConfigResponse {}) +} + +fn query_count(deps: Deps) -> StdResult { + let count = COUNT.load(deps.storage)?; + Ok(CountResponse { count }) +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..f4e0e69 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,11 @@ +pub mod contract; +pub mod error; +mod handlers; +pub mod msg; +mod replies; +pub mod state; + +#[cfg(feature = "interface")] +pub use contract::interface::AppInterface; +#[cfg(feature = "interface")] +pub use msg::{AppExecuteMsgFns, AppQueryMsgFns}; diff --git a/src/msg.rs b/src/msg.rs new file mode 100644 index 0000000..b0a0f0f --- /dev/null +++ b/src/msg.rs @@ -0,0 +1,51 @@ +use cosmwasm_schema::QueryResponses; + +use crate::contract::App; + +// This is used for type safety and re-exporting the contract endpoint structs. +abstract_app::app_msg_types!(App, AppExecuteMsg, AppQueryMsg); + +/// App instantiate message +#[cosmwasm_schema::cw_serde] +pub struct AppInstantiateMsg { + /// Initial count + pub count: i32, +} + +/// App execute messages +#[cosmwasm_schema::cw_serde] +#[cfg_attr(feature = "interface", derive(cw_orch::ExecuteFns))] +#[cfg_attr(feature = "interface", impl_into(ExecuteMsg))] +pub enum AppExecuteMsg { + /// Increment count by 1 + Increment {}, + /// Admin method - reset count + Reset { + /// Count value after reset + count: i32, + }, + UpdateConfig {}, +} + +/// App query messages +#[cosmwasm_schema::cw_serde] +#[cfg_attr(feature = "interface", derive(cw_orch::QueryFns))] +#[cfg_attr(feature = "interface", impl_into(QueryMsg))] +#[derive(QueryResponses)] +pub enum AppQueryMsg { + #[returns(ConfigResponse)] + Config {}, + #[returns(CountResponse)] + Count {}, +} + +#[cosmwasm_schema::cw_serde] +pub struct AppMigrateMsg {} + +#[cosmwasm_schema::cw_serde] +pub struct ConfigResponse {} + +#[cosmwasm_schema::cw_serde] +pub struct CountResponse { + pub count: i32, +} diff --git a/src/replies/instantiate.rs b/src/replies/instantiate.rs new file mode 100644 index 0000000..206238f --- /dev/null +++ b/src/replies/instantiate.rs @@ -0,0 +1,8 @@ +use crate::contract::{App, AppResult}; + +use abstract_app::traits::AbstractResponse; +use cosmwasm_std::{DepsMut, Env, Reply}; + +pub fn instantiate_reply(_deps: DepsMut, _env: Env, app: App, _reply: Reply) -> AppResult { + Ok(app.response("instantiate_reply")) +} diff --git a/src/replies/mod.rs b/src/replies/mod.rs new file mode 100644 index 0000000..cb1c54f --- /dev/null +++ b/src/replies/mod.rs @@ -0,0 +1,5 @@ +mod instantiate; + +pub use instantiate::instantiate_reply; + +pub const INSTANTIATE_REPLY_ID: u64 = 1u64; diff --git a/src/state.rs b/src/state.rs new file mode 100644 index 0000000..52b2a63 --- /dev/null +++ b/src/state.rs @@ -0,0 +1,7 @@ +use cw_storage_plus::Item; + +#[cosmwasm_schema::cw_serde] +pub struct Config {} + +pub const CONFIG: Item = Item::new("config"); +pub const COUNT: Item = Item::new("count"); diff --git a/template-setup.sh b/template-setup.sh new file mode 100755 index 0000000..fa48d15 --- /dev/null +++ b/template-setup.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash + +cp example.env .env + +# Function to prompt for 'y' input +prompt_confirmation() { + local prompt_message="$1" + read -p "$prompt_message (y/n): " -n 1 -r + echo # Move to a new line + [[ $REPLY =~ ^[Yy]$ ]] +} + +# Check if 'just' command is available +if ! command -v just &> /dev/null +then + echo "'just' command not found. 🤨" + + # Ask to install 'just' + if prompt_confirmation "Do you want to install the 'just' command runner?" + then + cargo install just + echo "'just' has been installed." + else + echo "Installation of 'just' cancelled. Can't install tools. ❌" + exit 0 + fi +fi + +# Ask to install tools using 'just' +if prompt_confirmation "Do you want to install tools (cargo-nextest, taplo-cli, cargo-watch, cargo-limit)?" +then + just install-tools + echo "Tools have been installed! 👷" +else + echo "Tools installation cancelled. ❌" + exit 0 +fi diff --git a/tests/integration.rs b/tests/integration.rs new file mode 100644 index 0000000..0fb4eb0 --- /dev/null +++ b/tests/integration.rs @@ -0,0 +1,89 @@ +use abstract_app::objects::namespace::Namespace; + +use abstract_client::AbstractClient; +use abstract_client::Application; + +use app::{ + contract::APP_ID, + error::AppError, + msg::{AppInstantiateMsg, ConfigResponse, CountResponse}, + *, +}; +use cw_controllers::AdminError; +// Use prelude to get all the necessary imports +use cw_orch::{anyhow, prelude::*}; + +use cosmwasm_std::{coins, Addr}; + +/// Set up the test environment with an Account that has the App installed +#[allow(clippy::type_complexity)] +fn setup( + count: i32, +) -> anyhow::Result<( + AbstractClient, + Application>, +)> { + // Create a sender and mock env + let mock = MockBech32::new("mock"); + let sender = mock.sender(); + let namespace = Namespace::from_id(APP_ID)?; + + // You can set up Abstract with a builder. + let client = AbstractClient::builder(mock).build()?; + // The client supports setting balances for addresses and configuring ANS. + client.set_balance(sender, &coins(123, "ucosm"))?; + + // Build a Publisher Account + let publisher = client.publisher_builder(namespace).build()?; + + publisher.publish_app::>()?; + + let app = publisher + .account() + .install_app::>(&AppInstantiateMsg { count }, &[])?; + + Ok((client, app)) +} + +#[test] +fn successful_install() -> anyhow::Result<()> { + let (_, app) = setup(0)?; + + let config = app.config()?; + assert_eq!(config, ConfigResponse {}); + Ok(()) +} + +#[test] +fn successful_increment() -> anyhow::Result<()> { + let (_, app) = setup(0)?; + + app.increment()?; + let count: CountResponse = app.count()?; + assert_eq!(count.count, 1); + Ok(()) +} + +#[test] +fn successful_reset() -> anyhow::Result<()> { + let (_, app) = setup(0)?; + + app.reset(42)?; + let count: CountResponse = app.count()?; + assert_eq!(count.count, 42); + Ok(()) +} + +#[test] +fn failed_reset() -> anyhow::Result<()> { + let (_, app) = setup(0)?; + + let err: AppError = app + .call_as(&Addr::unchecked("NotAdmin")) + .reset(9) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!(err, AppError::Admin(AdminError::NotAdmin {})); + Ok(()) +}