diff --git a/Cargo.lock b/Cargo.lock index 8e23f20c..ebe45581 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9,19 +9,19 @@ dependencies = [ "controller", "cosmwasm-schema", "cosmwasm-std", - "cosmwasm-storage", - "cw-asset", "cw-multi-test", - "cw-storage-plus 0.16.0", - "cw2 0.16.0", - "cw20", - "json-codec-wasm", "prost 0.11.9", "schemars", "serde", - "strum", - "strum_macros", - "thiserror", +] + +[[package]] +name = "account-tracker" +version = "0.1.0" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-multi-test", ] [[package]] @@ -37,9 +37,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.68" +version = "1.0.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cb2f989d18dd141ab8ae82f64d1a8cdd37e0840f73a406896cf5e99502fab61" +checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" [[package]] name = "base16ct" @@ -107,17 +107,15 @@ version = "0.1.0" dependencies = [ "cosmwasm-schema", "cosmwasm-std", - "cosmwasm-storage", - "cw-asset", "cw-multi-test", - "cw-storage-plus 0.16.0", - "cw2 0.16.0", "cw20", + "cw721", + "prost 0.11.9", "schemars", "serde", + "serde-json-wasm 0.4.1", "strum", "strum_macros", - "thiserror", ] [[package]] @@ -720,18 +718,7 @@ dependencies = [ "controller", "cosmwasm-schema", "cosmwasm-std", - "cosmwasm-storage", - "cw-asset", "cw-multi-test", - "cw-storage-plus 0.16.0", - "cw2 0.16.0", - "cw20", - "json-codec-wasm", - "schemars", - "serde", - "strum", - "strum_macros", - "thiserror", ] [[package]] @@ -965,19 +952,8 @@ dependencies = [ "controller", "cosmwasm-schema", "cosmwasm-std", - "cosmwasm-storage", - "cw-asset", "cw-multi-test", - "cw-storage-plus 0.16.0", - "cw2 0.16.0", - "cw20", - "json-codec-wasm", "resolver", - "schemars", - "serde", - "strum", - "strum_macros", - "thiserror", ] [[package]] @@ -1035,6 +1011,7 @@ name = "warp-account" version = "0.1.0" dependencies = [ "account", + "anyhow", "base64", "controller", "cosmwasm-schema", @@ -1043,6 +1020,31 @@ dependencies = [ "cw-asset", "cw-multi-test", "cw-storage-plus 0.16.0", + "cw-utils 0.16.0", + "cw2 0.16.0", + "cw20", + "cw721", + "json-codec-wasm", + "prost 0.11.9", + "schemars", + "serde-json-wasm 0.4.1", + "thiserror", +] + +[[package]] +name = "warp-account-tracker" +version = "0.1.0" +dependencies = [ + "account-tracker", + "anyhow", + "base64", + "cosmwasm-schema", + "cosmwasm-std", + "cosmwasm-storage", + "cw-asset", + "cw-multi-test", + "cw-storage-plus 0.16.0", + "cw-utils 0.16.0", "cw2 0.16.0", "cw20", "cw721", @@ -1058,6 +1060,7 @@ name = "warp-controller" version = "0.1.0" dependencies = [ "account", + "account-tracker", "base64", "controller", "cosmwasm-schema", @@ -1088,6 +1091,7 @@ dependencies = [ "cw-asset", "cw-multi-test", "cw-storage-plus 0.16.0", + "cw-utils 0.16.0", "cw2 0.16.0", "cw20", "cw721", diff --git a/Terra_Warp_Contracts_CosmWasm_Smart_Contract_Security_Assessment_Report_Halborn_Final.pdf b/Terra_Warp_Contracts_CosmWasm_Smart_Contract_Security_Assessment_Report_Halborn_Final.pdf new file mode 100644 index 00000000..0ba8c25f Binary files /dev/null and b/Terra_Warp_Contracts_CosmWasm_Smart_Contract_Security_Assessment_Report_Halborn_Final.pdf differ diff --git a/V2CHANGELOG.md b/V2CHANGELOG.md new file mode 100644 index 00000000..33c6d947 --- /dev/null +++ b/V2CHANGELOG.md @@ -0,0 +1,119 @@ +# V2 Changelog + +## Major Updates + +- **User’s Warp-Account Removed:** + - Replaced by job accounts. + - `create_job` now creates a job account on the fly in the same transaction. + - Users can provide funds in `info.funds` that are relayed to the job account. + - Job account is used as a job session throughout job execution. + - In case of recurring jobs, the same job account is kept through time. + - Required for stateful jobs like trading strategies. + +- **Multiple Funding Accounts:** + - Users can create and manage multiple funding accounts. + - Funding account is used for distributing rewards to keepers and paying fees. + - Otherwise, fees are subtracted from `info.funds`. + - Useful for recurring jobs to provide fees and topups on the side. + +- **New Fee Mechanism:** + - Introduces a more dynamic and flexible fee calculation system. + - Creation Fee: Calculated based on the queue size. + - Maintenance Fee: Determined based on the duration in days. + - Burn Fee: Computed from the job reward. + - `total_fees = creation_fee + maintenance_fee + burn_fee` + - `job cost = total_fees + reward` + +- **New Contract Warp-Account-Tracker:** + - Used for management of job accounts and funding accounts. + - Holds state for taken and free accounts by job_id. + +## API Changes + +### Removed +- `create_job.msgs` +- `create_job.condition` +- `create_job.requeue_on_evict` + +### Added +- `create_job.executions` + - Array of executions that operate like a switch. + - Single execution contains msgs (warp msgs) and condition. + - On job execution, the first execution condition that returns true top-down is taken. +- **Job Accounts with WarpMsg Struct:** + - Job accounts now operate with WarpMsg struct. + - Previously, warp accounts worked only with cosmos msgs. + - WarpMsg:Generic is equivalent to a cosmos msg. + - Added support for WarpMsg:WithdrawAssets and WarpMsg:IbcTransfer. + - Extensible messaging standard within warp in case custom message formats are needed in the future. +- `create_job.operational_amount` + - Without funding account: `operational_amount` needs to equal `total_fees + reward`. + - With funding account: Ignored, can be set to 0. +- `create_job.duration_days` + - Defines job length of stay in the warp queue. + - Maintenance fee paid upfront for it based on fee calculations. +- `create_job.cw_funds` + - Optionally passed list of cw20 and cw721 funds to be sent from user to job account. +- `create_job.funding_account` + - Optionally attached funding account from which job fees and rewards are deducted. + - Required for recurring jobs. + - Optionally provided for one-time jobs. +- `create_job.account_msgs` + - Messages that are executed via job-account on job creation. + - Useful for deploying funds to money markets to earn APR while the job waits for execution. +- **Controller.create_funding_account API:** + - Creates a new free funding account for the user. +- **FnValue StringValue Support:** + - Static variable can be initialized with an `init_fn`. + - FnValue now supports `StringValue`. + +## Fee functions + +### Creation Fee + +The creation fee (`f(qs)`) is a piecewise function depending on the queue size (`qs`). + +``` +f(qs) = + y1, if qs < x1 + slope * qs + y1 - slope * x1, if x1 <= qs < x2 + y2, if qs >= x2 +``` + +Where: +- `x1` = `config.queue_size_left` +- `x2` = `config.queue_size_right` +- `y1` = `config.creation_fee_min` +- `y2` = `config.creation_fee_max` +- `slope` = `(y2 - y1) / (x2 - x1)` + +### Maintenance Fee + +The maintenance fee (`g(dd)`) is structured similarly, based on the duration in days (`dd`). + +``` +g(dd) = + y1, if dd < x1 + slope * dd + y1 - slope * x1, if x1 <= dd < x2 + y2, if dd >= x2 +``` + +Where: +- `x1` = `config.duration_days_min` +- `x2` = `config.duration_days_max` +- `slope` = `(y2 - y1) / (x2 - x1)` + +### Burn Fee + +The burn fee (`h(job_reward)`) is calculated as the maximum between the `calculated_fee` and `min_fee`. + +``` +h(job_reward) = + max(calculated_fee, min_fee) +``` + +Where: +- `calculated_fee` = `job_reward * config.burn_fee_rate / 100` +- `min_fee` = `config.burn_fee_min` + + \ No newline at end of file diff --git a/contracts/warp-account-tracker/.cargo/config b/contracts/warp-account-tracker/.cargo/config new file mode 100644 index 00000000..86e5ea3e --- /dev/null +++ b/contracts/warp-account-tracker/.cargo/config @@ -0,0 +1,4 @@ +[alias] +wasm = "build --release --target wasm32-unknown-unknown" +unit-test = "test --lib" +schema = "run --example warp-account-tracker-schema" diff --git a/contracts/warp-account-tracker/.gitignore b/contracts/warp-account-tracker/.gitignore new file mode 100644 index 00000000..9095deaa --- /dev/null +++ b/contracts/warp-account-tracker/.gitignore @@ -0,0 +1,16 @@ +# Build results +/target +/schema + +# Cargo+Git helper file (https://github.com/rust-lang/cargo/blob/0.44.1/src/cargo/sources/git/utils.rs#L320-L327) +.cargo-ok + +# Text file backups +**/*.rs.bk + +# macOS +.DS_Store + +# IDEs +*.iml +.idea diff --git a/contracts/warp-account-tracker/Cargo.toml b/contracts/warp-account-tracker/Cargo.toml new file mode 100644 index 00000000..66bbc611 --- /dev/null +++ b/contracts/warp-account-tracker/Cargo.toml @@ -0,0 +1,52 @@ +[package] +name = "warp-account-tracker" +version = "0.1.0" +authors = ["Terra Money "] +edition = "2021" + +exclude = [ + # Those files are rust-optimizer artifacts. You might want to commit them for convenience but they should not be part of the source code publication. + "contract.wasm", + "hash.txt", +] + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +# for more explicit tests, cargo test --features=backtraces +backtraces = ["cosmwasm-std/backtraces"] +# use library feature to disable all instantiate/execute/query exports +library = [] + +[package.metadata.scripts] +optimize = """docker run --rm -v "${process.cwd()}":/code \ + -v "${path.join(process.cwd(), "../../", "packages")}":/packages \ + --mount type=volume,source="${contract}_cache",target=/code/target \ + --mount type=volume,source=registry_cache,target=/usr/local/cargo/registry \ + cosmwasm/rust-optimizer${process.env.TERRARIUM_ARCH_ARM64 ? "-arm64" : ""}:0.12.6 +""" + +[dependencies] +cosmwasm-std = "1.1" +cosmwasm-storage = "1.1" +cosmwasm-schema = "1.1" +base64 = "0.13.0" +cw-asset = "2.2" +cw-storage-plus = "0.16" +cw2 = "0.16" +cw20 = "0.16" +cw721 = "0.16.0" +cw-utils = "0.16" +account-tracker = { path = "../../packages/account-tracker", default-features = false, version = "*" } +schemars = "0.8" +thiserror = "1" +serde-json-wasm = "0.4.1" +json-codec-wasm = "0.1.0" +prost = "0.11.9" + +[dev-dependencies] +cw-multi-test = "0.16.0" +anyhow = "1.0.71" diff --git a/contracts/warp-account-tracker/README.md b/contracts/warp-account-tracker/README.md new file mode 100644 index 00000000..954383af --- /dev/null +++ b/contracts/warp-account-tracker/README.md @@ -0,0 +1,106 @@ +# CosmWasm Starter Pack + +This is a template to build smart contracts in Rust to run inside a +[Cosmos SDK](https://github.com/cosmos/cosmos-sdk) module on all chains that enable it. +To understand the framework better, please read the overview in the +[cosmwasm repo](https://github.com/CosmWasm/cosmwasm/blob/master/README.md), +and dig into the [cosmwasm docs](https://www.cosmwasm.com). +This assumes you understand the theory and just want to get coding. + +## Creating a new repo from template + +Assuming you have a recent version of rust and cargo (v1.58.1+) installed +(via [rustup](https://rustup.rs/)), +then the following should get you a new repo to start a contract: + +Install [cargo-generate](https://github.com/ashleygwilliams/cargo-generate) and cargo-run-script. +Unless you did that before, run this line now: + +```sh +cargo install cargo-generate --features vendored-openssl +cargo install cargo-run-script +``` + +Now, use it to create your new contract. +Go to the folder in which you want to place it and run: + + +**Latest: 1.0.0-beta6** + +```sh +cargo generate --git https://github.com/CosmWasm/cw-template.git --name PROJECT_NAME +```` + +**Older Version** + +Pass version as branch flag: + +```sh +cargo generate --git https://github.com/CosmWasm/cw-template.git --branch --name PROJECT_NAME +```` + +Example: + +```sh +cargo generate --git https://github.com/CosmWasm/cw-template.git --branch 0.16 --name PROJECT_NAME +``` + +You will now have a new folder called `PROJECT_NAME` (I hope you changed that to something else) +containing a simple working contract and build system that you can customize. + +## Create a Repo + +After generating, you have a initialized local git repo, but no commits, and no remote. +Go to a server (eg. github) and create a new upstream repo (called `YOUR-GIT-URL` below). +Then run the following: + +```sh +# this is needed to create a valid Cargo.lock file (see below) +cargo check +git branch -M main +git add . +git commit -m 'Initial Commit' +git remote add origin YOUR-GIT-URL +git push -u origin main +``` + +## CI Support + +We have template configurations for both [GitHub Actions](.github/workflows/Basic.yml) +and [Circle CI](.circleci/config.yml) in the generated project, so you can +get up and running with CI right away. + +One note is that the CI runs all `cargo` commands +with `--locked` to ensure it uses the exact same versions as you have locally. This also means +you must have an up-to-date `Cargo.lock` file, which is not auto-generated. +The first time you set up the project (or after adding any dep), you should ensure the +`Cargo.lock` file is updated, so the CI will test properly. This can be done simply by +running `cargo check` or `cargo unit-test`. + +## Using your project + +Once you have your custom repo, you should check out [Developing](./Developing.md) to explain +more on how to run tests and develop code. Or go through the +[online tutorial](https://docs.cosmwasm.com/) to get a better feel +of how to develop. + +[Publishing](./Publishing.md) contains useful information on how to publish your contract +to the world, once you are ready to deploy it on a running blockchain. And +[Importing](./Importing.md) contains information about pulling in other contracts or crates +that have been published. + +Please replace this README file with information about your specific project. You can keep +the `Developing.md` and `Publishing.md` files as useful referenced, but please set some +proper description in the README. + +## Gitpod integration + +[Gitpod](https://www.gitpod.io/) container-based development platform will be enabled on your project by default. + +Workspace contains: + - **rust**: for builds + - [wasmd](https://github.com/CosmWasm/wasmd): for local node setup and client + - **jq**: shell JSON manipulation tool + +Follow [Gitpod Getting Started](https://www.gitpod.io/docs/getting-started) and launch your workspace. + diff --git a/contracts/warp-account-tracker/examples/warp-account-tracker-schema.rs b/contracts/warp-account-tracker/examples/warp-account-tracker-schema.rs new file mode 100644 index 00000000..a5558f03 --- /dev/null +++ b/contracts/warp-account-tracker/examples/warp-account-tracker-schema.rs @@ -0,0 +1,27 @@ +use std::env::current_dir; +use std::fs::create_dir_all; + +use account_tracker::{ + Account, AccountsResponse, Config, ConfigResponse, ExecuteMsg, FundingAccountResponse, + FundingAccountsResponse, InstantiateMsg, JobAccountResponse, JobAccountsResponse, QueryMsg, +}; +use cosmwasm_schema::{export_schema, remove_schemas, schema_for}; + +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!(Config), &out_dir); + export_schema(&schema_for!(AccountsResponse), &out_dir); + export_schema(&schema_for!(FundingAccountResponse), &out_dir); + export_schema(&schema_for!(FundingAccountsResponse), &out_dir); + export_schema(&schema_for!(JobAccountResponse), &out_dir); + export_schema(&schema_for!(JobAccountsResponse), &out_dir); + export_schema(&schema_for!(ConfigResponse), &out_dir); + export_schema(&schema_for!(Account), &out_dir); +} diff --git a/contracts/warp-account-tracker/meta/README.md b/contracts/warp-account-tracker/meta/README.md new file mode 100644 index 00000000..279d1db4 --- /dev/null +++ b/contracts/warp-account-tracker/meta/README.md @@ -0,0 +1,16 @@ +# The meta folder + +This folder is ignored via the `.genignore` file. It contains meta files +that should not make it into the generated project. + +In particular, it is used for an AppVeyor CI script that runs on `cw-template` +itself (running the cargo-generate script, then testing the generated project). +The `.circleci` and `.github` directories contain scripts destined for any projects created from +this template. + +## Files + +- `appveyor.yml`: The AppVeyor CI configuration +- `test_generate.sh`: A script for generating a project from the template and + runnings builds and tests in it. This works almost like the CI script but + targets local UNIX-like dev environments. diff --git a/contracts/warp-account-tracker/meta/appveyor.yml b/contracts/warp-account-tracker/meta/appveyor.yml new file mode 100644 index 00000000..5da37f70 --- /dev/null +++ b/contracts/warp-account-tracker/meta/appveyor.yml @@ -0,0 +1,61 @@ +# This CI configuration tests the cw-template repository itself, +# not the resulting project. We want to ensure that +# 1. the template to project generation works +# 2. the template files are up to date +# +# We chose Appveyor for this task as it allows us to use an arbitrary config +# location. Furthermore it allows us to ship Circle CI and GitHub Actions configs +# generated for the resulting project. + +image: Ubuntu + +environment: + TOOLCHAIN: 1.58.1 + +services: + - docker + +cache: + - $HOME/.rustup/ -> meta/appveyor.yml + # For details about cargo caching see https://doc.rust-lang.org/cargo/guide/cargo-home.html#caching-the-cargo-home-in-ci + - $HOME/.cargo/bin/ -> meta/appveyor.yml + - $HOME/.cargo/registry/index/ -> meta/appveyor.yml + - $HOME/.cargo/registry/cache/ -> meta/appveyor.yml + - $HOME/.cargo/git/db/ -> meta/appveyor.yml + +install: + - curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- --default-toolchain "$TOOLCHAIN" -y + - source $HOME/.cargo/env + - rustc --version + - cargo --version + - rustup target add wasm32-unknown-unknown + - cargo install --features vendored-openssl cargo-generate || true + +build_script: + # No matter what is currently checked out by the CI (main, other branch, PR merge commit), + # we create a temporary local branch from that point with a constant name, which we need for + # cargo generate. + - git branch current-ci-checkout + - cd .. + - cargo generate --git cw-template --name testgen-ci --branch current-ci-checkout + - cd testgen-ci + - ls -lA + - cargo fmt -- --check + - cargo unit-test + - cargo wasm + - cargo schema + - docker build --pull -t "cosmwasm/cw-gitpod-base:${APPVEYOR_REPO_COMMIT}" . + - \[ "${APPVEYOR_REPO_BRANCH}" = "main" \] && image_tag=latest || image_tag=${APPVEYOR_REPO_TAG_NAME} + - docker tag "cosmwasm/cw-gitpod-base:${APPVEYOR_REPO_COMMIT}" "cosmwasm/cw-gitpod-base:${image_tag}" + +on_success: + # publish docker image + - docker login --password-stdin -u "$DOCKER_USER" <<<"$DOCKER_PASS" + - docker push + - docker logout + +branches: +# whitelist long living branches and tags + only: + # - main + - /v\d+\.\d+\.\d+/ diff --git a/contracts/warp-account-tracker/meta/test_generate.sh b/contracts/warp-account-tracker/meta/test_generate.sh new file mode 100644 index 00000000..b9aaa237 --- /dev/null +++ b/contracts/warp-account-tracker/meta/test_generate.sh @@ -0,0 +1,37 @@ +#!/bin/bash +set -o errexit -o nounset -o pipefail +command -v shellcheck > /dev/null && shellcheck "$0" + +REPO_ROOT="$(realpath "$(dirname "$0")/..")" + +TMP_DIR=$(mktemp -d "${TMPDIR:-/tmp}/cw-template.XXXXXXXXX") +PROJECT_NAME="testgen-local" + +( + echo "Navigating to $TMP_DIR" + cd "$TMP_DIR" + + GIT_BRANCH=$(git -C "$REPO_ROOT" branch --show-current) + + echo "Generating project from local repository (branch $GIT_BRANCH) ..." + cargo generate --git "$REPO_ROOT" --name "$PROJECT_NAME" --branch "$GIT_BRANCH" + + ( + cd "$PROJECT_NAME" + echo "This is what was generated" + ls -lA + + # Check formatting + echo "Checking formatting ..." + cargo fmt -- --check + + # Debug builds first to fail fast + echo "Running unit tests ..." + cargo unit-test + echo "Creating schema ..." + cargo schema + + echo "Building wasm ..." + cargo wasm + ) +) diff --git a/contracts/warp-account-tracker/src/contract.rs b/contracts/warp-account-tracker/src/contract.rs new file mode 100644 index 00000000..99732274 --- /dev/null +++ b/contracts/warp-account-tracker/src/contract.rs @@ -0,0 +1,97 @@ +use crate::execute::config::update_config; +use crate::state::CONFIG; +use crate::{execute, query, ContractError}; +use account_tracker::{Config, ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}; +use cosmwasm_std::{ + entry_point, to_binary, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult, +}; +use cw_utils::nonpayable; + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn instantiate( + deps: DepsMut, + env: Env, + _info: MessageInfo, + msg: InstantiateMsg, +) -> Result { + let instantiated_account_addr = env.contract.address; + + CONFIG.save( + deps.storage, + &Config { + admin: deps.api.addr_validate(&msg.admin)?, + warp_addr: deps.api.addr_validate(&msg.warp_addr)?, + }, + )?; + + Ok(Response::new() + .add_attribute("action", "instantiate") + .add_attribute("contract_addr", instantiated_account_addr.clone()) + .add_attribute("account_tracker", instantiated_account_addr) + .add_attribute("admin", msg.admin) + .add_attribute("warp_addr", msg.warp_addr)) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn execute( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: ExecuteMsg, +) -> Result { + let config = CONFIG.load(deps.storage)?; + if info.sender != config.admin && info.sender != config.warp_addr { + return Err(ContractError::Unauthorized {}); + } + + match msg { + ExecuteMsg::TakeJobAccount(data) => { + nonpayable(&info).unwrap(); + execute::account::take_job_account(deps, data) + } + ExecuteMsg::FreeJobAccount(data) => { + nonpayable(&info).unwrap(); + execute::account::free_job_account(deps, data) + } + ExecuteMsg::TakeFundingAccount(data) => { + nonpayable(&info).unwrap(); + execute::account::take_funding_account(deps, data) + } + ExecuteMsg::FreeFundingAccount(data) => { + nonpayable(&info).unwrap(); + execute::account::free_funding_account(deps, data) + } + ExecuteMsg::UpdateConfig(data) => update_config(deps, env, info, data), + } +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { + match msg { + QueryMsg::QueryConfig(_) => to_binary(&query::account::query_config(deps)?), + QueryMsg::QueryAccounts(data) => to_binary(&query::account::query_accounts(deps, data)?), + QueryMsg::QueryFundingAccounts(data) => { + to_binary(&query::account::query_funding_accounts(deps, data)?) + } + QueryMsg::QueryFundingAccount(data) => { + to_binary(&query::account::query_funding_account(deps, data)?) + } + QueryMsg::QueryFirstFreeFundingAccount(data) => to_binary( + &query::account::query_first_free_funding_account(deps, data)?, + ), + QueryMsg::QueryJobAccounts(data) => { + to_binary(&query::account::query_job_accounts(deps, data)?) + } + QueryMsg::QueryJobAccount(data) => { + to_binary(&query::account::query_job_account(deps, data)?) + } + QueryMsg::QueryFirstFreeJobAccount(data) => { + to_binary(&query::account::query_first_free_job_account(deps, data)?) + } + } +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn migrate(_deps: DepsMut, _env: Env, _msg: MigrateMsg) -> Result { + Ok(Response::new()) +} diff --git a/contracts/warp-account-tracker/src/error.rs b/contracts/warp-account-tracker/src/error.rs new file mode 100644 index 00000000..fe78cc67 --- /dev/null +++ b/contracts/warp-account-tracker/src/error.rs @@ -0,0 +1,79 @@ +use crate::ContractError::{DecodeError, DeserializationError, SerializationError}; +use cosmwasm_std::StdError; +use thiserror::Error; + +#[derive(Error, Debug, PartialEq)] +pub enum ContractError { + #[error("{0}")] + Std(#[from] StdError), + + #[error("Unauthorized")] + Unauthorized {}, + + #[error("Invalid fee")] + InvalidFee {}, + + #[error("Funds array in message does not match funds array in job.")] + FundsMismatch {}, + + #[error("Reward provided is smaller than minimum")] + RewardTooSmall {}, + + #[error("Invalid arguments")] + InvalidArguments {}, + + #[error("Custom Error val: {val:?}")] + CustomError { val: String }, + // Add any other custom errors you like here. + // Look at https://docs.rs/thiserror/1.0.21/thiserror/ for details. + #[error("Error deserializing data")] + DeserializationError {}, + + #[error("Error serializing data")] + SerializationError {}, + + #[error("Error decoding JSON result")] + DecodeError {}, + + #[error("Error resolving JSON path")] + ResolveError {}, + + #[error("Account already taken")] + AccountAlreadyTakenError {}, + + #[error("Account already free")] + AccountAlreadyFreeError {}, + + #[error("Account should be taken but it is free")] + AccountNotTakenError {}, + + #[error("Account not found")] + AccountNotFound {}, + + #[error("Invalid account type")] + InvalidAccountType {}, +} + +impl From for ContractError { + fn from(_: serde_json_wasm::de::Error) -> Self { + DeserializationError {} + } +} + +impl From for ContractError { + fn from(_: serde_json_wasm::ser::Error) -> Self { + SerializationError {} + } +} + +impl From for ContractError { + fn from(_: json_codec_wasm::DecodeError) -> Self { + DecodeError {} + } +} + +impl From for ContractError { + fn from(_: base64::DecodeError) -> Self { + DecodeError {} + } +} diff --git a/contracts/warp-account-tracker/src/execute/account.rs b/contracts/warp-account-tracker/src/execute/account.rs new file mode 100644 index 00000000..75f951a6 --- /dev/null +++ b/contracts/warp-account-tracker/src/execute/account.rs @@ -0,0 +1,201 @@ +use crate::state::{ + ACCOUNTS, FREE_FUNDING_ACCOUNTS, FREE_JOB_ACCOUNTS, TAKEN_FUNDING_ACCOUNTS, TAKEN_JOB_ACCOUNTS, +}; +use crate::ContractError; +use account_tracker::{ + Account, AccountType, FreeFundingAccountMsg, FreeJobAccountMsg, TakeFundingAccountMsg, + TakeJobAccountMsg, +}; +use cosmwasm_std::{DepsMut, Response, Uint64}; + +pub fn take_job_account(deps: DepsMut, data: TakeJobAccountMsg) -> Result { + let account_owner_ref = &deps.api.addr_validate(data.account_owner_addr.as_str())?; + let account_addr_ref = &deps.api.addr_validate(data.account_addr.as_str())?; + + // Attempt to load the account; if it doesn't exist, create a new one + let account = ACCOUNTS.update( + deps.storage, + (account_owner_ref, account_addr_ref), + |s| -> Result { + match s { + Some(account) => Ok(account), + None => Ok(Account { + account_type: AccountType::Job, + owner_addr: account_owner_ref.clone(), + account_addr: account_addr_ref.clone(), + }), + } + }, + )?; + + if account.account_type != AccountType::Job { + return Err(ContractError::InvalidAccountType {}); + } + + FREE_JOB_ACCOUNTS.remove(deps.storage, (account_owner_ref, account_addr_ref)); + TAKEN_JOB_ACCOUNTS.update( + deps.storage, + (account_owner_ref, account_addr_ref), + |s| match s { + None => Ok(data.job_id), + Some(_) => Err(ContractError::AccountAlreadyTakenError {}), + }, + )?; + + Ok(Response::new() + .add_attribute("action", "take_job_account") + .add_attribute("account_addr", data.account_addr) + .add_attribute("job_id", data.job_id.to_string())) +} + +pub fn free_job_account(deps: DepsMut, data: FreeJobAccountMsg) -> Result { + let account_owner_ref = &deps.api.addr_validate(data.account_owner_addr.as_str())?; + let account_addr_ref = &deps.api.addr_validate(data.account_addr.as_str())?; + + // Attempt to load the account; if it doesn't exist, create a new one + let account = ACCOUNTS.update( + deps.storage, + (account_owner_ref, account_addr_ref), + |s| -> Result { + match s { + Some(account) => Ok(account), + None => Ok(Account { + account_type: AccountType::Job, + owner_addr: account_owner_ref.clone(), + account_addr: account_addr_ref.clone(), + }), + } + }, + )?; + + if account.account_type != AccountType::Job { + return Err(ContractError::InvalidAccountType {}); + } + + TAKEN_JOB_ACCOUNTS.remove(deps.storage, (account_owner_ref, account_addr_ref)); + FREE_JOB_ACCOUNTS.update( + deps.storage, + (account_owner_ref, account_addr_ref), + |s| match s { + None => Ok(data.last_job_id), + Some(_) => Err(ContractError::AccountAlreadyFreeError {}), + }, + )?; + + Ok(Response::new() + .add_attribute("action", "free_job_account") + .add_attribute("account_addr", data.account_addr)) +} + +pub fn take_funding_account( + deps: DepsMut, + data: TakeFundingAccountMsg, +) -> Result { + let account_owner_addr_ref = &deps.api.addr_validate(&data.account_owner_addr)?; + let account_addr_ref = &deps.api.addr_validate(data.account_addr.as_str())?; + + // Attempt to load the account; if it doesn't exist, create a new one + let account = ACCOUNTS.update( + deps.storage, + (account_owner_addr_ref, account_addr_ref), + |s| -> Result { + match s { + Some(account) => Ok(account), + None => Ok(Account { + account_type: AccountType::Funding, + owner_addr: account_owner_addr_ref.clone(), + account_addr: account_addr_ref.clone(), + }), + } + }, + )?; + + if account.account_type != AccountType::Funding { + return Err(ContractError::InvalidAccountType {}); + } + + FREE_FUNDING_ACCOUNTS.remove(deps.storage, (account_owner_addr_ref, account_addr_ref)); + TAKEN_FUNDING_ACCOUNTS.update( + deps.storage, + (account_owner_addr_ref, account_addr_ref), + |ids| -> Result, ContractError> { + match ids { + Some(mut id_list) => { + id_list.push(data.job_id); + Ok(id_list) + } + None => Ok(vec![data.job_id]), + } + }, + )?; + + Ok(Response::new() + .add_attribute("action", "take_funding_account") + .add_attribute("account_addr", data.account_addr) + .add_attribute("job_id", data.job_id.to_string())) +} + +pub fn free_funding_account( + deps: DepsMut, + data: FreeFundingAccountMsg, +) -> Result { + let account_owner_addr_ref = &deps.api.addr_validate(&data.account_owner_addr)?; + let account_addr_ref = &deps.api.addr_validate(&data.account_addr)?; + + // Attempt to load the account; if it doesn't exist, create a new one + let account = ACCOUNTS.update( + deps.storage, + (account_owner_addr_ref, account_addr_ref), + |s| -> Result { + match s { + Some(account) => Ok(account), + None => Ok(Account { + account_type: AccountType::Funding, + owner_addr: account_owner_addr_ref.clone(), + account_addr: account_addr_ref.clone(), + }), + } + }, + )?; + + if account.account_type != AccountType::Funding { + return Err(ContractError::InvalidAccountType {}); + } + + // Retrieve current job IDs for the funding account + let mut job_ids = match TAKEN_FUNDING_ACCOUNTS + .may_load(deps.storage, (account_owner_addr_ref, account_addr_ref)) + { + Ok(Some(job_ids)) => job_ids, + Ok(None) => vec![], + Err(err) => return Err(ContractError::Std(err)), + }; + + // Remove the specified job ID + job_ids.retain(|&id| id != data.job_id); + + // Update or remove the entry in TAKEN_FUNDING_ACCOUNTS based on the updated list + if job_ids.is_empty() { + TAKEN_FUNDING_ACCOUNTS.remove(deps.storage, (account_owner_addr_ref, account_addr_ref)); + FREE_FUNDING_ACCOUNTS.update( + deps.storage, + (account_owner_addr_ref, account_addr_ref), + |s| match s { + None => Ok(vec![data.job_id]), + Some(_) => Err(ContractError::AccountAlreadyFreeError {}), + }, + )?; + } else { + // Update the entry in TAKEN_FUNDING_ACCOUNTS + TAKEN_FUNDING_ACCOUNTS.save( + deps.storage, + (account_owner_addr_ref, account_addr_ref), + &job_ids, + )?; + } + + Ok(Response::new() + .add_attribute("action", "free_funding_account") + .add_attribute("account_addr", data.account_addr) + .add_attribute("job_id", data.job_id.to_string())) +} diff --git a/contracts/warp-account-tracker/src/execute/config.rs b/contracts/warp-account-tracker/src/execute/config.rs new file mode 100644 index 00000000..ed0640f4 --- /dev/null +++ b/contracts/warp-account-tracker/src/execute/config.rs @@ -0,0 +1,28 @@ +use account_tracker::UpdateConfigMsg; +use cosmwasm_std::{DepsMut, Env, MessageInfo, Response}; + +use crate::{state::CONFIG, ContractError}; + +pub fn update_config( + deps: DepsMut, + _env: Env, + info: MessageInfo, + data: UpdateConfigMsg, +) -> Result { + let mut config = CONFIG.load(deps.storage)?; + + if info.sender != config.admin { + return Err(ContractError::Unauthorized {}); + } + + config.admin = match data.admin { + None => config.admin, + Some(data) => deps.api.addr_validate(data.as_str())?, + }; + + CONFIG.save(deps.storage, &config)?; + + Ok(Response::new() + .add_attribute("action", "update_config") + .add_attribute("config_admin", config.admin)) +} diff --git a/contracts/warp-account-tracker/src/execute/mod.rs b/contracts/warp-account-tracker/src/execute/mod.rs new file mode 100644 index 00000000..6038438e --- /dev/null +++ b/contracts/warp-account-tracker/src/execute/mod.rs @@ -0,0 +1,2 @@ +pub(crate) mod account; +pub(crate) mod config; diff --git a/contracts/warp-account-tracker/src/integration_tests.rs b/contracts/warp-account-tracker/src/integration_tests.rs new file mode 100644 index 00000000..90faf8a3 --- /dev/null +++ b/contracts/warp-account-tracker/src/integration_tests.rs @@ -0,0 +1,391 @@ +#[cfg(test)] +mod tests { + use account_tracker::{ + AccountStatus, Config, ConfigResponse, ExecuteMsg, FreeJobAccountMsg, InstantiateMsg, + JobAccount, JobAccountResponse, JobAccountsResponse, QueryConfigMsg, + QueryFirstFreeJobAccountMsg, QueryJobAccountsMsg, QueryMsg, TakeJobAccountMsg, + }; + use anyhow::Result as AnyResult; + use cosmwasm_std::{Addr, Coin, Empty, Uint128, Uint64}; + use cw_multi_test::{App, AppBuilder, AppResponse, Contract, ContractWrapper, Executor}; + + use crate::{ + contract::{execute, instantiate, query}, + ContractError, + }; + + const DUMMY_WARP_CONTROLLER_ADDR: &str = "terra1"; + const USER_1: &str = "terra2"; + const DUMMY_WARP_ACCOUNT_1_ADDR: &str = "terra3"; + const DUMMY_WARP_ACCOUNT_2_ADDR: &str = "terra4"; + const DUMMY_WARP_ACCOUNT_3_ADDR: &str = "terra5"; + const DUMMY_JOB_1_ID: Uint64 = Uint64::zero(); + const DUMMY_JOB_2_ID: Uint64 = Uint64::one(); + + fn mock_app() -> App { + AppBuilder::new().build(|router, _, storage| { + router + .bank + .init_balance( + storage, + &Addr::unchecked(USER_1), + vec![Coin { + denom: "uluna".to_string(), + // 1_000_000_000 uLuna i.e. 1k LUNA since 1 LUNA = 1_000_000 uLuna + amount: Uint128::new(1_000_000_000), + }], + ) + .unwrap(); + }) + } + + fn contract_warp_account_tracker() -> Box> { + let contract = ContractWrapper::new(execute, instantiate, query); + Box::new(contract) + } + + fn init_warp_account_tracker( + app: &mut App, + warp_account_tracker_contract_code_id: u64, + ) -> Addr { + app.instantiate_contract( + warp_account_tracker_contract_code_id, + Addr::unchecked(DUMMY_WARP_CONTROLLER_ADDR), + &InstantiateMsg { + admin: USER_1.to_string(), + warp_addr: DUMMY_WARP_CONTROLLER_ADDR.to_string(), + }, + &[], + "warp_account_tracker", + None, + ) + .unwrap() + } + + fn assert_err(res: AnyResult, err: ContractError) { + match res { + Ok(_) => panic!("Result was not an error"), + Err(generic_err) => { + let contract_err: ContractError = generic_err.downcast().unwrap(); + assert_eq!(contract_err, err); + } + } + } + + #[test] + fn warp_account_tracker_contract_multi_test_account_management() { + let mut app = mock_app(); + let warp_account_tracker_contract_code_id = app.store_code(contract_warp_account_tracker()); + + // Instantiate account + let warp_account_tracker_contract_addr = + init_warp_account_tracker(&mut app, warp_account_tracker_contract_code_id); + assert_eq!( + app.wrap().query_wasm_smart( + warp_account_tracker_contract_addr.clone(), + &QueryMsg::QueryConfig(QueryConfigMsg {}) + ), + Ok(ConfigResponse { + config: Config { + admin: Addr::unchecked(USER_1), + warp_addr: Addr::unchecked(DUMMY_WARP_CONTROLLER_ADDR), + } + }) + ); + assert_eq!( + app.wrap().query_wasm_smart( + warp_account_tracker_contract_addr.clone(), + &QueryMsg::QueryFirstFreeJobAccount(QueryFirstFreeJobAccountMsg { + account_owner_addr: USER_1.to_string(), + }) + ), + Ok(JobAccountResponse { job_account: None }) + ); + assert_eq!( + app.wrap().query_wasm_smart( + warp_account_tracker_contract_addr.clone(), + &QueryMsg::QueryJobAccounts(QueryJobAccountsMsg { + account_owner_addr: USER_1.to_string(), + start_after: None, + limit: None, + account_status: AccountStatus::Free + }) + ), + Ok(JobAccountsResponse { + job_accounts: vec![], + total_count: 0 + }) + ); + assert_eq!( + app.wrap().query_wasm_smart( + warp_account_tracker_contract_addr.clone(), + &QueryMsg::QueryJobAccounts(QueryJobAccountsMsg { + account_owner_addr: USER_1.to_string(), + start_after: None, + limit: None, + account_status: AccountStatus::Taken + }) + ), + Ok(JobAccountsResponse { + job_accounts: vec![], + total_count: 0 + }) + ); + + // Mark first account as free + let _ = app.execute_contract( + Addr::unchecked(USER_1), + warp_account_tracker_contract_addr.clone(), + &ExecuteMsg::FreeJobAccount(FreeJobAccountMsg { + account_owner_addr: USER_1.to_string(), + account_addr: DUMMY_WARP_ACCOUNT_1_ADDR.to_string(), + last_job_id: DUMMY_JOB_1_ID, + }), + &[], + ); + + // Cannot free account twice + assert_err( + app.execute_contract( + Addr::unchecked(USER_1), + warp_account_tracker_contract_addr.clone(), + &ExecuteMsg::FreeJobAccount(FreeJobAccountMsg { + account_owner_addr: USER_1.to_string(), + account_addr: DUMMY_WARP_ACCOUNT_1_ADDR.to_string(), + last_job_id: DUMMY_JOB_1_ID, + }), + &[], + ), + ContractError::AccountAlreadyFreeError {}, + ); + + // Mark second account as free + let _ = app.execute_contract( + Addr::unchecked(USER_1), + warp_account_tracker_contract_addr.clone(), + &ExecuteMsg::FreeJobAccount(FreeJobAccountMsg { + account_owner_addr: USER_1.to_string(), + account_addr: DUMMY_WARP_ACCOUNT_2_ADDR.to_string(), + last_job_id: DUMMY_JOB_2_ID, + }), + &[], + ); + + // Mark third account as free + let _ = app.execute_contract( + Addr::unchecked(USER_1), + warp_account_tracker_contract_addr.clone(), + &ExecuteMsg::FreeJobAccount(FreeJobAccountMsg { + account_owner_addr: USER_1.to_string(), + account_addr: DUMMY_WARP_ACCOUNT_3_ADDR.to_string(), + last_job_id: DUMMY_JOB_1_ID, + }), + &[], + ); + + // Query first free account + assert_eq!( + app.wrap().query_wasm_smart( + warp_account_tracker_contract_addr.clone(), + &QueryMsg::QueryFirstFreeJobAccount(QueryFirstFreeJobAccountMsg { + account_owner_addr: USER_1.to_string(), + }) + ), + Ok(JobAccountResponse { + job_account: Some(JobAccount { + account_addr: Addr::unchecked(DUMMY_WARP_ACCOUNT_1_ADDR), + taken_by_job_id: DUMMY_JOB_1_ID, + account_status: AccountStatus::Free + }) + }) + ); + + // Query free accounts + assert_eq!( + app.wrap().query_wasm_smart( + warp_account_tracker_contract_addr.clone(), + &QueryMsg::QueryJobAccounts(QueryJobAccountsMsg { + account_owner_addr: USER_1.to_string(), + start_after: None, + limit: None, + account_status: AccountStatus::Free + }) + ), + Ok(JobAccountsResponse { + job_accounts: vec![ + JobAccount { + account_addr: Addr::unchecked(DUMMY_WARP_ACCOUNT_1_ADDR), + taken_by_job_id: DUMMY_JOB_1_ID, + account_status: AccountStatus::Free + }, + JobAccount { + account_addr: Addr::unchecked(DUMMY_WARP_ACCOUNT_2_ADDR), + taken_by_job_id: DUMMY_JOB_2_ID, + account_status: AccountStatus::Free + }, + JobAccount { + account_addr: Addr::unchecked(DUMMY_WARP_ACCOUNT_3_ADDR), + taken_by_job_id: DUMMY_JOB_1_ID, + account_status: AccountStatus::Free + }, + ], + total_count: 3 + }) + ); + + // Query taken accounts + assert_eq!( + app.wrap().query_wasm_smart( + warp_account_tracker_contract_addr.clone(), + &QueryMsg::QueryJobAccounts(QueryJobAccountsMsg { + account_owner_addr: USER_1.to_string(), + start_after: None, + limit: None, + account_status: AccountStatus::Taken + }) + ), + Ok(JobAccountsResponse { + job_accounts: vec![], + total_count: 0 + }) + ); + + // Take second account with job 1 + let _ = app.execute_contract( + Addr::unchecked(USER_1), + warp_account_tracker_contract_addr.clone(), + &ExecuteMsg::TakeJobAccount(TakeJobAccountMsg { + account_owner_addr: USER_1.to_string(), + account_addr: DUMMY_WARP_ACCOUNT_2_ADDR.to_string(), + job_id: DUMMY_JOB_1_ID, + }), + &[], + ); + + // Cannot take account twice + assert_err( + app.execute_contract( + Addr::unchecked(USER_1), + warp_account_tracker_contract_addr.clone(), + &ExecuteMsg::TakeJobAccount(TakeJobAccountMsg { + account_owner_addr: USER_1.to_string(), + account_addr: DUMMY_WARP_ACCOUNT_2_ADDR.to_string(), + job_id: DUMMY_JOB_2_ID, + }), + &[], + ), + ContractError::AccountAlreadyTakenError {}, + ); + + // Query free accounts + assert_eq!( + app.wrap().query_wasm_smart( + warp_account_tracker_contract_addr.clone(), + &QueryMsg::QueryJobAccounts(QueryJobAccountsMsg { + account_owner_addr: USER_1.to_string(), + start_after: None, + limit: None, + account_status: AccountStatus::Free + }) + ), + Ok(JobAccountsResponse { + job_accounts: vec![ + JobAccount { + account_addr: Addr::unchecked(DUMMY_WARP_ACCOUNT_1_ADDR), + taken_by_job_id: DUMMY_JOB_1_ID, + account_status: AccountStatus::Free + }, + JobAccount { + account_addr: Addr::unchecked(DUMMY_WARP_ACCOUNT_3_ADDR), + taken_by_job_id: DUMMY_JOB_1_ID, + account_status: AccountStatus::Free + }, + ], + total_count: 2 + }) + ); + + // Query taken accounts + assert_eq!( + app.wrap().query_wasm_smart( + warp_account_tracker_contract_addr.clone(), + &QueryMsg::QueryJobAccounts(QueryJobAccountsMsg { + account_owner_addr: USER_1.to_string(), + start_after: None, + limit: None, + account_status: AccountStatus::Taken + }) + ), + Ok(JobAccountsResponse { + job_accounts: vec![JobAccount { + account_addr: Addr::unchecked(DUMMY_WARP_ACCOUNT_2_ADDR), + taken_by_job_id: DUMMY_JOB_1_ID, + account_status: AccountStatus::Taken + },], + total_count: 1 + }) + ); + + // Free second account + let _ = app.execute_contract( + Addr::unchecked(USER_1), + warp_account_tracker_contract_addr.clone(), + &ExecuteMsg::FreeJobAccount(FreeJobAccountMsg { + account_owner_addr: USER_1.to_string(), + account_addr: DUMMY_WARP_ACCOUNT_2_ADDR.to_string(), + last_job_id: DUMMY_JOB_1_ID, + }), + &[], + ); + + // Query free accounts + assert_eq!( + app.wrap().query_wasm_smart( + warp_account_tracker_contract_addr.clone(), + &QueryMsg::QueryJobAccounts(QueryJobAccountsMsg { + account_owner_addr: USER_1.to_string(), + start_after: None, + limit: None, + account_status: AccountStatus::Free + }) + ), + Ok(JobAccountsResponse { + job_accounts: vec![ + JobAccount { + account_addr: Addr::unchecked(DUMMY_WARP_ACCOUNT_1_ADDR), + taken_by_job_id: DUMMY_JOB_1_ID, + account_status: AccountStatus::Free + }, + JobAccount { + account_addr: Addr::unchecked(DUMMY_WARP_ACCOUNT_2_ADDR), + taken_by_job_id: DUMMY_JOB_1_ID, + account_status: AccountStatus::Free + }, + JobAccount { + account_addr: Addr::unchecked(DUMMY_WARP_ACCOUNT_3_ADDR), + taken_by_job_id: DUMMY_JOB_1_ID, + account_status: AccountStatus::Free + }, + ], + total_count: 3 + }) + ); + + // Query taken accounts + assert_eq!( + app.wrap().query_wasm_smart( + warp_account_tracker_contract_addr, + &QueryMsg::QueryJobAccounts(QueryJobAccountsMsg { + account_owner_addr: USER_1.to_string(), + start_after: None, + limit: None, + account_status: AccountStatus::Taken + }) + ), + Ok(JobAccountsResponse { + job_accounts: vec![], + total_count: 0 + }) + ); + } +} diff --git a/contracts/warp-account-tracker/src/lib.rs b/contracts/warp-account-tracker/src/lib.rs new file mode 100644 index 00000000..71a7ed70 --- /dev/null +++ b/contracts/warp-account-tracker/src/lib.rs @@ -0,0 +1,10 @@ +pub mod contract; +mod error; +mod execute; +mod query; +pub mod state; + +#[cfg(test)] +mod integration_tests; + +pub use crate::error::ContractError; diff --git a/contracts/warp-account-tracker/src/query/account.rs b/contracts/warp-account-tracker/src/query/account.rs new file mode 100644 index 00000000..94f19269 --- /dev/null +++ b/contracts/warp-account-tracker/src/query/account.rs @@ -0,0 +1,288 @@ +use cosmwasm_std::{Deps, Order, StdResult}; +use cw_storage_plus::{Bound, PrefixBound}; + +use crate::state::{ + ACCOUNTS, CONFIG, FREE_FUNDING_ACCOUNTS, FREE_JOB_ACCOUNTS, TAKEN_FUNDING_ACCOUNTS, + TAKEN_JOB_ACCOUNTS, +}; + +use account_tracker::{ + Account, AccountStatus, AccountsResponse, ConfigResponse, FundingAccount, + FundingAccountResponse, FundingAccountsResponse, JobAccount, JobAccountResponse, + JobAccountsResponse, QueryAccountsMsg, QueryFirstFreeFundingAccountMsg, + QueryFirstFreeJobAccountMsg, QueryFundingAccountMsg, QueryFundingAccountsMsg, + QueryJobAccountMsg, QueryJobAccountsMsg, +}; + +const QUERY_LIMIT: u32 = 50; + +pub fn query_config(deps: Deps) -> StdResult { + let config = CONFIG.load(deps.storage)?; + Ok(ConfigResponse { config }) +} + +pub fn query_accounts(deps: Deps, data: QueryAccountsMsg) -> StdResult { + let account_owner_ref = &deps.api.addr_validate(data.account_owner_addr.as_str())?; + let start_after = data + .start_after + .map(|addr| deps.api.addr_validate(&addr)) + .transpose()?; + + let iter = match start_after { + Some(start_after_addr) => ACCOUNTS.range( + deps.storage, + Some(Bound::exclusive((account_owner_ref, &start_after_addr))), + None, + Order::Ascending, + ), + None => ACCOUNTS.prefix_range( + deps.storage, + Some(PrefixBound::inclusive(account_owner_ref)), + Some(PrefixBound::inclusive(account_owner_ref)), + Order::Ascending, + ), + }; + + let accounts = iter + .take(data.limit.unwrap_or(QUERY_LIMIT) as usize) + .map(|item| item.map(|(_, account)| account)) + .collect::>>()?; + + Ok(AccountsResponse { accounts }) +} + +pub fn query_funding_account( + deps: Deps, + data: QueryFundingAccountMsg, +) -> StdResult { + let account_owner_addr_ref = &deps.api.addr_validate(data.account_owner_addr.as_str())?; + let account_addr_ref = &deps.api.addr_validate(data.account_addr.as_str())?; + + let funding_account = match TAKEN_FUNDING_ACCOUNTS + .may_load(deps.storage, (account_owner_addr_ref, account_addr_ref)) + { + Ok(Some(job_ids)) => Some(FundingAccount { + account_addr: account_addr_ref.clone(), + taken_by_job_ids: job_ids, + account_status: AccountStatus::Taken, + }), + Ok(None) => { + match FREE_FUNDING_ACCOUNTS + .may_load(deps.storage, (account_owner_addr_ref, account_addr_ref)) + { + Ok(Some(job_ids)) => Some(FundingAccount { + account_addr: account_addr_ref.clone(), + taken_by_job_ids: job_ids, + account_status: AccountStatus::Free, + }), + Ok(None) => None, + Err(err) => return Err(err), + } + } + Err(err) => return Err(err), + }; + + Ok(FundingAccountResponse { funding_account }) +} + +pub fn query_first_free_funding_account( + deps: Deps, + data: QueryFirstFreeFundingAccountMsg, +) -> StdResult { + let resp = query_funding_accounts( + deps, + QueryFundingAccountsMsg { + account_owner_addr: data.account_owner_addr, + account_status: AccountStatus::Free, + start_after: None, + limit: Some(1), + }, + )?; + + Ok(FundingAccountResponse { + funding_account: resp.funding_accounts.first().cloned(), + }) +} + +pub fn query_funding_accounts( + deps: Deps, + data: QueryFundingAccountsMsg, +) -> StdResult { + let account_owner_ref = &deps.api.addr_validate(data.account_owner_addr.as_str())?; + let status = data.account_status; + + let iter = match status { + AccountStatus::Free => match data.start_after { + Some(start_after) => { + let start_after_account_addr = &deps.api.addr_validate(start_after.as_str())?; + FREE_FUNDING_ACCOUNTS.range( + deps.storage, + Some(Bound::exclusive(( + account_owner_ref, + start_after_account_addr, + ))), + None, + Order::Ascending, + ) + } + None => FREE_FUNDING_ACCOUNTS.prefix_range( + deps.storage, + Some(PrefixBound::inclusive(account_owner_ref)), + Some(PrefixBound::inclusive(account_owner_ref)), + Order::Ascending, + ), + }, + AccountStatus::Taken => match data.start_after { + Some(start_after) => { + let start_after_account_addr = &deps.api.addr_validate(start_after.as_str())?; + TAKEN_FUNDING_ACCOUNTS.range( + deps.storage, + Some(Bound::exclusive(( + account_owner_ref, + start_after_account_addr, + ))), + None, + Order::Ascending, + ) + } + None => TAKEN_FUNDING_ACCOUNTS.prefix_range( + deps.storage, + Some(PrefixBound::inclusive(account_owner_ref)), + Some(PrefixBound::inclusive(account_owner_ref)), + Order::Ascending, + ), + }, + }; + + let funding_accounts = iter + .take(data.limit.unwrap_or(QUERY_LIMIT) as usize) + .map(|item| { + item.map(|(account, job_ids)| FundingAccount { + account_addr: account.1, + taken_by_job_ids: job_ids, + account_status: status.clone(), + }) + }) + .collect::>>()?; + + Ok(FundingAccountsResponse { + funding_accounts: funding_accounts.clone(), + total_count: funding_accounts.len() as u32, + }) +} + +pub fn query_job_accounts(deps: Deps, data: QueryJobAccountsMsg) -> StdResult { + let account_owner_ref = &deps.api.addr_validate(data.account_owner_addr.as_str())?; + let status = data.account_status; + + let iter = match status { + AccountStatus::Free => match data.start_after { + Some(start_after) => { + let start_after_account_addr = &deps.api.addr_validate(start_after.as_str())?; + + FREE_JOB_ACCOUNTS.range( + deps.storage, + Some(Bound::exclusive(( + account_owner_ref, + start_after_account_addr, + ))), + None, + Order::Ascending, + ) + } + None => FREE_JOB_ACCOUNTS.prefix_range( + deps.storage, + Some(PrefixBound::inclusive(account_owner_ref)), + Some(PrefixBound::inclusive(account_owner_ref)), + Order::Ascending, + ), + }, + AccountStatus::Taken => match data.start_after { + Some(start_after) => { + let start_after_account_addr = &deps.api.addr_validate(start_after.as_str())?; + + TAKEN_JOB_ACCOUNTS.range( + deps.storage, + Some(Bound::exclusive(( + account_owner_ref, + start_after_account_addr, + ))), + None, + Order::Ascending, + ) + } + None => TAKEN_JOB_ACCOUNTS.prefix_range( + deps.storage, + Some(PrefixBound::inclusive(account_owner_ref)), + Some(PrefixBound::inclusive(account_owner_ref)), + Order::Ascending, + ), + }, + }; + + let job_accounts = iter + .take(data.limit.unwrap_or(QUERY_LIMIT) as usize) + .map(|item| { + item.map(|(account, job_id)| JobAccount { + account_addr: account.1, + taken_by_job_id: job_id, + account_status: status.clone(), + }) + }) + .collect::>>()?; + + Ok(JobAccountsResponse { + job_accounts: job_accounts.clone(), + total_count: job_accounts.len() as u32, + }) +} + +pub fn query_job_account(deps: Deps, data: QueryJobAccountMsg) -> StdResult { + let account_owner_addr_ref = &deps.api.addr_validate(data.account_owner_addr.as_str())?; + let account_addr_ref = &deps.api.addr_validate(data.account_addr.as_str())?; + + let job_account = match TAKEN_JOB_ACCOUNTS + .may_load(deps.storage, (account_owner_addr_ref, account_addr_ref)) + { + Ok(Some(job_id)) => Some(JobAccount { + account_addr: account_addr_ref.clone(), + taken_by_job_id: job_id, + account_status: AccountStatus::Taken, + }), + Ok(None) => { + match FREE_JOB_ACCOUNTS + .may_load(deps.storage, (account_owner_addr_ref, account_addr_ref)) + { + Ok(Some(job_id)) => Some(JobAccount { + account_addr: account_addr_ref.clone(), + taken_by_job_id: job_id, + account_status: AccountStatus::Free, + }), + Ok(None) => None, + Err(err) => return Err(err), + } + } + Err(err) => return Err(err), + }; + + Ok(JobAccountResponse { job_account }) +} + +pub fn query_first_free_job_account( + deps: Deps, + data: QueryFirstFreeJobAccountMsg, +) -> StdResult { + let resp = query_job_accounts( + deps, + QueryJobAccountsMsg { + account_owner_addr: data.account_owner_addr, + account_status: AccountStatus::Free, + start_after: None, + limit: Some(1), + }, + )?; + + Ok(JobAccountResponse { + job_account: resp.job_accounts.first().cloned(), + }) +} diff --git a/contracts/warp-account-tracker/src/query/mod.rs b/contracts/warp-account-tracker/src/query/mod.rs new file mode 100644 index 00000000..d937534a --- /dev/null +++ b/contracts/warp-account-tracker/src/query/mod.rs @@ -0,0 +1 @@ +pub(crate) mod account; diff --git a/contracts/warp-account-tracker/src/state.rs b/contracts/warp-account-tracker/src/state.rs new file mode 100644 index 00000000..77b501a3 --- /dev/null +++ b/contracts/warp-account-tracker/src/state.rs @@ -0,0 +1,22 @@ +use account_tracker::{Account, Config}; +use cosmwasm_std::{Addr, Uint64}; +use cw_storage_plus::{Item, Map}; + +pub const CONFIG: Item = Item::new("config"); + +// Key is the (account owner address, account address), value is the account struct +pub const ACCOUNTS: Map<(&Addr, &Addr), Account> = Map::new("accounts"); + +// Key is the (account owner address, account address), value is a vector of IDs of the jobs currently using it +pub const TAKEN_FUNDING_ACCOUNTS: Map<(&Addr, &Addr), Vec> = + Map::new("taken_funding_accounts"); + +// Key is the (account owner address, account address), value is id of the last job which reserved it (vec[last_job_id]) +pub const FREE_FUNDING_ACCOUNTS: Map<(&Addr, &Addr), Vec> = + Map::new("free_funding_accounts"); + +// Key is the (account owner address, account address), value is the ID of the pending job currently using it +pub const TAKEN_JOB_ACCOUNTS: Map<(&Addr, &Addr), Uint64> = Map::new("taken_job_accounts"); + +// Key is the (account owner address, account address), value is id of the last job which reserved it +pub const FREE_JOB_ACCOUNTS: Map<(&Addr, &Addr), Uint64> = Map::new("free_job_accounts"); diff --git a/contracts/warp-account/Cargo.toml b/contracts/warp-account/Cargo.toml index 8b70222b..94cd2c23 100644 --- a/contracts/warp-account/Cargo.toml +++ b/contracts/warp-account/Cargo.toml @@ -39,6 +39,7 @@ cw-storage-plus = "0.16" cw2 = "0.16" cw20 = "0.16" cw721 = "0.16.0" +cw-utils = "0.16" controller = { path = "../../packages/controller", default-features = false, version = "*" } account = { path = "../../packages/account", default-features = false, version = "*" } schemars = "0.8" @@ -49,3 +50,4 @@ prost = "0.11.9" [dev-dependencies] cw-multi-test = "0.16.0" +anyhow = "1.0.71" diff --git a/contracts/warp-account/examples/warp-account-schema.rs b/contracts/warp-account/examples/warp-account-schema.rs index b35a3eae..74b58663 100644 --- a/contracts/warp-account/examples/warp-account-schema.rs +++ b/contracts/warp-account/examples/warp-account-schema.rs @@ -1,11 +1,7 @@ use std::env::current_dir; use std::fs::create_dir_all; -use account::{Config, ExecuteMsg, InstantiateMsg}; -use controller::{ - account::{AccountResponse, AccountsResponse}, - job::{JobResponse, JobsResponse}, -}; +use account::{Config, ExecuteMsg, InstantiateMsg, QueryMsg}; use cosmwasm_schema::{export_schema, remove_schemas, schema_for}; fn main() { @@ -16,9 +12,6 @@ fn main() { 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!(Config), &out_dir); - export_schema(&schema_for!(JobResponse), &out_dir); - export_schema(&schema_for!(JobsResponse), &out_dir); - export_schema(&schema_for!(AccountResponse), &out_dir); - export_schema(&schema_for!(AccountsResponse), &out_dir); } diff --git a/contracts/warp-account/src/contract.rs b/contracts/warp-account/src/contract.rs index daa191d5..79438124 100644 --- a/contracts/warp-account/src/contract.rs +++ b/contracts/warp-account/src/contract.rs @@ -1,18 +1,10 @@ use crate::state::CONFIG; -use crate::ContractError; -use account::{ - Config, ExecuteMsg, IbcTransferMsg, InstantiateMsg, MigrateMsg, QueryMsg, TimeoutBlock, - WithdrawAssetsMsg, -}; -use controller::account::{AssetInfo, Cw721ExecuteMsg}; -use cosmwasm_std::CosmosMsg::Stargate; +use crate::{query, ContractError}; +use account::{Config, ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}; +use controller::account::{execute_warp_msgs, warp_msgs_to_cosmos_msgs}; use cosmwasm_std::{ - entry_point, to_binary, Addr, BankMsg, Binary, CosmosMsg, Deps, DepsMut, Env, MessageInfo, - Response, StdResult, Uint128, WasmMsg, + entry_point, to_binary, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult, }; -use cw20::{BalanceResponse, Cw20ExecuteMsg}; -use cw721::{Cw721QueryMsg, OwnerOfResponse}; -use prost::Message; #[cfg_attr(not(feature = "library"), entry_point)] pub fn instantiate( @@ -21,19 +13,28 @@ pub fn instantiate( info: MessageInfo, msg: InstantiateMsg, ) -> Result { - CONFIG.save( - deps.storage, - &Config { - owner: deps.api.addr_validate(&msg.owner)?, - warp_addr: info.sender, - }, - )?; + let instantiated_account_addr = env.contract.address.clone(); + let config = Config { + owner: deps.api.addr_validate(&msg.owner)?, + creator_addr: info.sender, + }; + + CONFIG.save(deps.storage, &config)?; + + let msgs = warp_msgs_to_cosmos_msgs(deps.as_ref(), env, msg.msgs, &config.owner).unwrap(); + Ok(Response::new() + .add_messages(msgs.clone()) .add_attribute("action", "instantiate") - .add_attribute("contract_addr", env.contract.address) + .add_attribute("job_id", msg.job_id) + .add_attribute("contract_addr", instantiated_account_addr) .add_attribute("owner", msg.owner) - .add_attribute("funds", serde_json_wasm::to_string(&info.funds)?) - .add_attribute("cw_funds", serde_json_wasm::to_string(&msg.funds)?)) + .add_attribute( + "native_funds", + serde_json_wasm::to_string(&msg.native_funds)?, + ) + .add_attribute("cw_funds", serde_json_wasm::to_string(&msg.cw_funds)?) + .add_attribute("account_msgs", serde_json_wasm::to_string(&msgs)?)) } #[cfg_attr(not(feature = "library"), entry_point)] @@ -44,25 +45,20 @@ pub fn execute( msg: ExecuteMsg, ) -> Result { let config = CONFIG.load(deps.storage)?; - if info.sender != config.owner && info.sender != config.warp_addr { + if info.sender != config.owner && info.sender != config.creator_addr { return Err(ContractError::Unauthorized {}); } match msg { - ExecuteMsg::Generic(data) => Ok(Response::new() - .add_messages(data.msgs) - .add_attribute("action", "generic")), - ExecuteMsg::WithdrawAssets(data) => withdraw_assets(deps, env, info, data), - ExecuteMsg::IbcTransfer(data) => ibc_transfer(deps, env, info, data), + ExecuteMsg::WarpMsgs(data) => { + execute_warp_msgs(deps, env, data, &config.owner).map_err(ContractError::Std) + } } } #[cfg_attr(not(feature = "library"), entry_point)] pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { match msg { - QueryMsg::Config => { - let config = CONFIG.load(deps.storage)?; - to_binary(&config) - } + QueryMsg::QueryConfig(_) => to_binary(&query::account::query_config(deps)?), } } @@ -70,165 +66,3 @@ pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { pub fn migrate(_deps: DepsMut, _env: Env, _msg: MigrateMsg) -> Result { Ok(Response::new()) } - -pub fn ibc_transfer( - _deps: DepsMut, - env: Env, - _info: MessageInfo, - msg: IbcTransferMsg, -) -> Result { - let mut transfer_msg = msg.transfer_msg.clone(); - - if msg.timeout_block_delta.is_some() && msg.transfer_msg.timeout_block.is_some() { - let block = transfer_msg.timeout_block.unwrap(); - transfer_msg.timeout_block = Some(TimeoutBlock { - revision_number: Some(block.revision_number()), - revision_height: Some(env.block.height + msg.timeout_block_delta.unwrap()), - }) - } - - if msg.timeout_timestamp_seconds_delta.is_some() { - transfer_msg.timeout_timestamp = Some( - env.block - .time - .plus_seconds( - env.block.time.seconds() + msg.timeout_timestamp_seconds_delta.unwrap(), - ) - .nanos(), - ); - } - - Ok(Response::new().add_message(Stargate { - type_url: "/ibc.applications.transfer.v1.MsgTransfer".to_string(), - value: transfer_msg.encode_to_vec().into(), - })) -} - -pub fn withdraw_assets( - deps: DepsMut, - env: Env, - info: MessageInfo, - data: WithdrawAssetsMsg, -) -> Result { - let config = CONFIG.load(deps.storage)?; - if info.sender != config.owner && info.sender != config.warp_addr { - return Err(ContractError::Unauthorized {}); - } - - let mut withdraw_msgs: Vec = vec![]; - - for asset_info in &data.asset_infos { - match asset_info { - AssetInfo::Native(denom) => { - let withdraw_native_msg = - withdraw_asset_native(deps.as_ref(), env.clone(), &config.owner, denom)?; - - match withdraw_native_msg { - None => {} - Some(msg) => withdraw_msgs.push(msg), - } - } - AssetInfo::Cw20(addr) => { - let withdraw_cw20_msg = - withdraw_asset_cw20(deps.as_ref(), env.clone(), &config.owner, addr)?; - - match withdraw_cw20_msg { - None => {} - Some(msg) => withdraw_msgs.push(msg), - } - } - AssetInfo::Cw721(addr, token_id) => { - let withdraw_cw721_msg = - withdraw_asset_cw721(deps.as_ref(), &config.owner, addr, token_id)?; - match withdraw_cw721_msg { - None => {} - Some(msg) => withdraw_msgs.push(msg), - } - } - } - } - - Ok(Response::new() - .add_messages(withdraw_msgs) - .add_attribute("action", "withdraw_assets") - .add_attribute("assets", serde_json_wasm::to_string(&data.asset_infos)?)) -} - -fn withdraw_asset_native( - deps: Deps, - env: Env, - owner: &Addr, - denom: &String, -) -> StdResult> { - let amount = deps.querier.query_balance(env.contract.address, denom)?; - - let res = if amount.amount > Uint128::zero() { - Some(CosmosMsg::Bank(BankMsg::Send { - to_address: owner.to_string(), - amount: vec![amount], - })) - } else { - None - }; - - Ok(res) -} - -fn withdraw_asset_cw20( - deps: Deps, - env: Env, - owner: &Addr, - token: &Addr, -) -> StdResult> { - let amount: BalanceResponse = deps.querier.query_wasm_smart( - token.to_string(), - &cw20::Cw20QueryMsg::Balance { - address: env.contract.address.to_string(), - }, - )?; - - let res = if amount.balance > Uint128::zero() { - Some(CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: token.to_string(), - msg: to_binary(&Cw20ExecuteMsg::Transfer { - recipient: owner.to_string(), - amount: amount.balance, - })?, - funds: vec![], - })) - } else { - None - }; - - Ok(res) -} - -fn withdraw_asset_cw721( - deps: Deps, - owner: &Addr, - token: &Addr, - token_id: &String, -) -> StdResult> { - let owner_query: OwnerOfResponse = deps.querier.query_wasm_smart( - token.to_string(), - &Cw721QueryMsg::OwnerOf { - token_id: token_id.to_string(), - include_expired: None, - }, - )?; - - let res = if owner_query.owner == *owner { - Some(CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: token.to_string(), - msg: to_binary(&Cw721ExecuteMsg::TransferNft { - recipient: owner.to_string(), - token_id: token_id.to_string(), - })?, - funds: vec![], - })) - } else { - None - }; - - Ok(res) -} diff --git a/contracts/warp-account/src/error.rs b/contracts/warp-account/src/error.rs index f8855692..85ae8cd0 100644 --- a/contracts/warp-account/src/error.rs +++ b/contracts/warp-account/src/error.rs @@ -37,6 +37,15 @@ pub enum ContractError { #[error("Error resolving JSON path")] ResolveError {}, + + #[error("Sub account already taken")] + SubAccountAlreadyTakenError {}, + + #[error("Sub account already free")] + SubAccountAlreadyFreeError {}, + + #[error("Sub account should be taken but it is free")] + SubAccountNotTakenError {}, } impl From for ContractError { diff --git a/contracts/warp-account/src/lib.rs b/contracts/warp-account/src/lib.rs index 90d6bfa8..dfce1919 100644 --- a/contracts/warp-account/src/lib.rs +++ b/contracts/warp-account/src/lib.rs @@ -1,5 +1,6 @@ pub mod contract; mod error; +mod query; pub mod state; #[cfg(test)] diff --git a/contracts/warp-account/src/query/account.rs b/contracts/warp-account/src/query/account.rs new file mode 100644 index 00000000..74bd5671 --- /dev/null +++ b/contracts/warp-account/src/query/account.rs @@ -0,0 +1,8 @@ +use crate::state::CONFIG; +use account::ConfigResponse; +use cosmwasm_std::{Deps, StdResult}; + +pub fn query_config(deps: Deps) -> StdResult { + let config = CONFIG.load(deps.storage)?; + Ok(ConfigResponse { config }) +} diff --git a/contracts/warp-account/src/query/mod.rs b/contracts/warp-account/src/query/mod.rs new file mode 100644 index 00000000..d937534a --- /dev/null +++ b/contracts/warp-account/src/query/mod.rs @@ -0,0 +1 @@ +pub(crate) mod account; diff --git a/contracts/warp-account/src/state.rs b/contracts/warp-account/src/state.rs index 3e4be73e..dc761dde 100644 --- a/contracts/warp-account/src/state.rs +++ b/contracts/warp-account/src/state.rs @@ -1,4 +1,5 @@ -use account::Config; use cw_storage_plus::Item; +use account::Config; + pub const CONFIG: Item = Item::new("config"); diff --git a/contracts/warp-account/src/tests.rs b/contracts/warp-account/src/tests.rs index 24ee92bc..3d1375e1 100644 --- a/contracts/warp-account/src/tests.rs +++ b/contracts/warp-account/src/tests.rs @@ -1,10 +1,11 @@ use crate::contract::{execute, instantiate}; use crate::ContractError; -use account::{ExecuteMsg, GenericMsg, InstantiateMsg}; +use account::{ExecuteMsg, InstantiateMsg}; +use controller::account::{WarpMsg, WarpMsgs}; use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info}; use cosmwasm_std::{ to_binary, BankMsg, Coin, CosmosMsg, DistributionMsg, GovMsg, IbcMsg, IbcTimeout, - IbcTimeoutBlock, Response, StakingMsg, Uint128, VoteOption, WasmMsg, + IbcTimeoutBlock, Response, StakingMsg, Uint128, Uint64, VoteOption, WasmMsg, }; #[test] @@ -19,42 +20,47 @@ fn test_execute_controller() { info.clone(), InstantiateMsg { owner: "vlad".to_string(), - funds: None, + job_id: Uint64::zero(), + native_funds: vec![], + cw_funds: vec![], + msgs: vec![], }, ); - let execute_msg = ExecuteMsg::Generic(GenericMsg { + let execute_msg = ExecuteMsg::WarpMsgs(WarpMsgs { msgs: vec![ - CosmosMsg::Wasm(WasmMsg::Execute { + WarpMsg::Generic(CosmosMsg::Wasm(WasmMsg::Execute { contract_addr: "contract".to_string(), msg: to_binary("test").unwrap(), funds: vec![Coin { denom: "coin".to_string(), amount: Uint128::new(100), }], - }), - CosmosMsg::Bank(BankMsg::Send { + })), + WarpMsg::Generic(CosmosMsg::Bank(BankMsg::Send { to_address: "vlad2".to_string(), amount: vec![Coin { denom: "coin".to_string(), amount: Uint128::new(100), }], - }), - CosmosMsg::Gov(GovMsg::Vote { + })), + WarpMsg::Generic(CosmosMsg::Gov(GovMsg::Vote { proposal_id: 0, vote: VoteOption::Yes, - }), - CosmosMsg::Staking(StakingMsg::Delegate { + })), + WarpMsg::Generic(CosmosMsg::Staking(StakingMsg::Delegate { validator: "vladidator".to_string(), amount: Coin { denom: "coin".to_string(), amount: Uint128::new(100), }, - }), - CosmosMsg::Distribution(DistributionMsg::SetWithdrawAddress { - address: "vladdress".to_string(), - }), - CosmosMsg::Ibc(IbcMsg::Transfer { + })), + WarpMsg::Generic(CosmosMsg::Distribution( + DistributionMsg::SetWithdrawAddress { + address: "vladdress".to_string(), + }, + )), + WarpMsg::Generic(CosmosMsg::Ibc(IbcMsg::Transfer { channel_id: "channel_vlad".to_string(), to_address: "vlad3".to_string(), amount: Coin { @@ -65,12 +71,13 @@ fn test_execute_controller() { revision: 0, height: 0, }), - }), - CosmosMsg::Stargate { + })), + WarpMsg::Generic(CosmosMsg::Stargate { type_url: "utl".to_string(), value: Default::default(), - }, + }), ], + job_id: None, }); let execute_res = execute(deps.as_mut(), env, info, execute_msg).unwrap(); @@ -78,7 +85,7 @@ fn test_execute_controller() { assert_eq!( execute_res, Response::new() - .add_attribute("action", "generic") + .add_attribute("action", "warp_msgs") .add_messages(vec![ CosmosMsg::Wasm(WasmMsg::Execute { contract_addr: "contract".to_string(), @@ -141,42 +148,47 @@ fn test_execute_owner() { info, InstantiateMsg { owner: "vlad".to_string(), - funds: None, + job_id: Uint64::zero(), + native_funds: vec![], + cw_funds: vec![], + msgs: vec![], }, ); - let execute_msg = ExecuteMsg::Generic(GenericMsg { + let execute_msg = ExecuteMsg::WarpMsgs(WarpMsgs { msgs: vec![ - CosmosMsg::Wasm(WasmMsg::Execute { + WarpMsg::Generic(CosmosMsg::Wasm(WasmMsg::Execute { contract_addr: "contract".to_string(), msg: to_binary("test").unwrap(), funds: vec![Coin { denom: "coin".to_string(), amount: Uint128::new(100), }], - }), - CosmosMsg::Bank(BankMsg::Send { + })), + WarpMsg::Generic(CosmosMsg::Bank(BankMsg::Send { to_address: "vlad2".to_string(), amount: vec![Coin { denom: "coin".to_string(), amount: Uint128::new(100), }], - }), - CosmosMsg::Gov(GovMsg::Vote { + })), + WarpMsg::Generic(CosmosMsg::Gov(GovMsg::Vote { proposal_id: 0, vote: VoteOption::Yes, - }), - CosmosMsg::Staking(StakingMsg::Delegate { + })), + WarpMsg::Generic(CosmosMsg::Staking(StakingMsg::Delegate { validator: "vladidator".to_string(), amount: Coin { denom: "coin".to_string(), amount: Uint128::new(100), }, - }), - CosmosMsg::Distribution(DistributionMsg::SetWithdrawAddress { - address: "vladdress".to_string(), - }), - CosmosMsg::Ibc(IbcMsg::Transfer { + })), + WarpMsg::Generic(CosmosMsg::Distribution( + DistributionMsg::SetWithdrawAddress { + address: "vladdress".to_string(), + }, + )), + WarpMsg::Generic(CosmosMsg::Ibc(IbcMsg::Transfer { channel_id: "channel_vlad".to_string(), to_address: "vlad3".to_string(), amount: Coin { @@ -187,12 +199,13 @@ fn test_execute_owner() { revision: 0, height: 0, }), - }), - CosmosMsg::Stargate { + })), + WarpMsg::Generic(CosmosMsg::Stargate { type_url: "utl".to_string(), value: Default::default(), - }, + }), ], + job_id: None, }); let info2 = mock_info("vlad", &[]); @@ -202,7 +215,7 @@ fn test_execute_owner() { assert_eq!( execute_res, Response::new() - .add_attribute("action", "generic") + .add_attribute("action", "warp_msgs") .add_messages(vec![ CosmosMsg::Wasm(WasmMsg::Execute { contract_addr: "contract".to_string(), @@ -265,42 +278,47 @@ fn test_execute_unauth() { info, InstantiateMsg { owner: "vlad".to_string(), - funds: None, + job_id: Uint64::zero(), + native_funds: vec![], + cw_funds: vec![], + msgs: vec![], }, ); - let execute_msg = ExecuteMsg::Generic(GenericMsg { + let execute_msg = ExecuteMsg::WarpMsgs(WarpMsgs { msgs: vec![ - CosmosMsg::Wasm(WasmMsg::Execute { + WarpMsg::Generic(CosmosMsg::Wasm(WasmMsg::Execute { contract_addr: "contract".to_string(), msg: to_binary("test").unwrap(), funds: vec![Coin { denom: "coin".to_string(), amount: Uint128::new(100), }], - }), - CosmosMsg::Bank(BankMsg::Send { + })), + WarpMsg::Generic(CosmosMsg::Bank(BankMsg::Send { to_address: "vlad2".to_string(), amount: vec![Coin { denom: "coin".to_string(), amount: Uint128::new(100), }], - }), - CosmosMsg::Gov(GovMsg::Vote { + })), + WarpMsg::Generic(CosmosMsg::Gov(GovMsg::Vote { proposal_id: 0, vote: VoteOption::Yes, - }), - CosmosMsg::Staking(StakingMsg::Delegate { + })), + WarpMsg::Generic(CosmosMsg::Staking(StakingMsg::Delegate { validator: "vladidator".to_string(), amount: Coin { denom: "coin".to_string(), amount: Uint128::new(100), }, - }), - CosmosMsg::Distribution(DistributionMsg::SetWithdrawAddress { - address: "vladdress".to_string(), - }), - CosmosMsg::Ibc(IbcMsg::Transfer { + })), + WarpMsg::Generic(CosmosMsg::Distribution( + DistributionMsg::SetWithdrawAddress { + address: "vladdress".to_string(), + }, + )), + WarpMsg::Generic(CosmosMsg::Ibc(IbcMsg::Transfer { channel_id: "channel_vlad".to_string(), to_address: "vlad3".to_string(), amount: Coin { @@ -311,12 +329,13 @@ fn test_execute_unauth() { revision: 0, height: 0, }), - }), - CosmosMsg::Stargate { + })), + WarpMsg::Generic(CosmosMsg::Stargate { type_url: "utl".to_string(), value: Default::default(), - }, + }), ], + job_id: None, }); let info2 = mock_info("vlad2", &[]); diff --git a/contracts/warp-controller/Cargo.toml b/contracts/warp-controller/Cargo.toml index f9de292f..5bd8c1b2 100644 --- a/contracts/warp-controller/Cargo.toml +++ b/contracts/warp-controller/Cargo.toml @@ -40,6 +40,7 @@ cw-utils = "0.16" cw2 = "0.16" cw20 = "0.16" account = { path = "../../packages/account", default-features = false, version = "*" } +account-tracker = { path = "../../packages/account-tracker", default-features = false, version = "*" } controller = { path = "../../packages/controller", default-features = false, version = "*" } resolver = { path = "../../packages/resolver", default-features = false, version = "*" } schemars = "0.8" diff --git a/contracts/warp-controller/examples/warp-controller-schema.rs b/contracts/warp-controller/examples/warp-controller-schema.rs index 4876d400..961b7ab8 100644 --- a/contracts/warp-controller/examples/warp-controller-schema.rs +++ b/contracts/warp-controller/examples/warp-controller-schema.rs @@ -2,9 +2,8 @@ use std::env::current_dir; use std::fs::create_dir_all; use controller::{ - account::{AccountResponse, AccountsResponse}, job::{JobResponse, JobsResponse}, - QueryMsg, {Config, ConfigResponse, ExecuteMsg, InstantiateMsg}, + QueryMsg, State, StateResponse, {Config, ConfigResponse, ExecuteMsg, InstantiateMsg}, }; use cosmwasm_schema::{export_schema, remove_schemas, schema_for}; @@ -19,8 +18,8 @@ fn main() { export_schema(&schema_for!(QueryMsg), &out_dir); export_schema(&schema_for!(Config), &out_dir); export_schema(&schema_for!(ConfigResponse), &out_dir); + export_schema(&schema_for!(State), &out_dir); + export_schema(&schema_for!(StateResponse), &out_dir); export_schema(&schema_for!(JobResponse), &out_dir); export_schema(&schema_for!(JobsResponse), &out_dir); - export_schema(&schema_for!(AccountResponse), &out_dir); - export_schema(&schema_for!(AccountsResponse), &out_dir); } diff --git a/contracts/warp-controller/src/contract.rs b/contracts/warp-controller/src/contract.rs index 79b49096..33d6fdfb 100644 --- a/contracts/warp-controller/src/contract.rs +++ b/contracts/warp-controller/src/contract.rs @@ -1,26 +1,28 @@ -use crate::error::map_contract_error; -use crate::state::{ACCOUNTS, CONFIG, FINISHED_JOBS, PENDING_JOBS}; -use crate::{execute, query, state::STATE, ContractError}; -use account::{GenericMsg, WithdrawAssetsMsg}; -use controller::account::{Account, Fund, FundTransferMsgs, TransferFromMsg, TransferNftMsg}; -use controller::job::{Job, JobStatus}; - -use controller::{Config, ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg, State}; use cosmwasm_std::{ - entry_point, to_binary, Attribute, BalanceResponse, BankMsg, BankQuery, Binary, Coin, - CosmosMsg, Deps, DepsMut, Env, MessageInfo, QueryRequest, Reply, Response, StdError, StdResult, - SubMsgResult, Uint128, Uint64, WasmMsg, + entry_point, to_binary, Binary, Deps, DepsMut, Env, MessageInfo, Reply, ReplyOn, Response, + StdResult, SubMsg, Uint64, +}; +use cw_utils::nonpayable; + +use crate::{ + execute, migrate, query, reply, + state::{CONFIG, STATE}, + util::msg::build_instantiate_account_tracker_msg, + ContractError, }; +use controller::{Config, ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg, State}; + #[cfg_attr(not(feature = "library"), entry_point)] pub fn instantiate( deps: DepsMut, - _env: Env, + env: Env, info: MessageInfo, msg: InstantiateMsg, ) -> Result { let state = State { - current_job_id: Uint64::one(), + // first 10 slots reserved for reply calls + current_job_id: Uint64::new(10u64), q: Uint64::zero(), }; @@ -34,40 +36,68 @@ pub fn instantiate( .addr_validate(&msg.fee_collector.unwrap_or_else(|| info.sender.to_string()))?, warp_account_code_id: msg.warp_account_code_id, minimum_reward: msg.minimum_reward, - creation_fee_percentage: msg.creation_fee, - cancellation_fee_percentage: msg.cancellation_fee, resolver_address: deps.api.addr_validate(&msg.resolver_address)?, - t_max: msg.t_max, - t_min: msg.t_min, - a_max: msg.a_max, - a_min: msg.a_min, - q_max: msg.q_max, + // placeholder, will be updated in reply + account_tracker_address: deps.api.addr_validate(&msg.resolver_address)?, + creation_fee_min: msg.creation_fee_min, + creation_fee_max: msg.creation_fee_max, + burn_fee_min: msg.burn_fee_min, + maintenance_fee_min: msg.maintenance_fee_min, + maintenance_fee_max: msg.maintenance_fee_max, + duration_days_min: msg.duration_days_min, + duration_days_max: msg.duration_days_max, + duration_days_limit: msg.duration_days_limit, + queue_size_left: msg.queue_size_left, + queue_size_right: msg.queue_size_right, + burn_fee_rate: msg.burn_fee_rate, + cancellation_fee_rate: msg.cancellation_fee_rate, }; - if config.a_max < config.a_min { - return Err(ContractError::MaxFeeUnderMinFee {}); + if config.creation_fee_max < config.creation_fee_min { + return Err(ContractError::CreationMaxFeeUnderMinFee {}); } - if config.t_max < config.t_min { - return Err(ContractError::MaxTimeUnderMinTime {}); + if config.maintenance_fee_max < config.maintenance_fee_min { + return Err(ContractError::MaintenanceMaxFeeUnderMinFee {}); } - if config.minimum_reward < config.a_min { - return Err(ContractError::RewardSmallerThanFee {}); + if config.duration_days_max <= config.duration_days_min { + return Err(ContractError::DurationMaxDaysUnderMinDays {}); } - if config.creation_fee_percentage.u64() > 100 { - return Err(ContractError::CreationFeeTooHigh {}); + if config.cancellation_fee_rate.u64() > 100 { + return Err(ContractError::CancellationFeeTooHigh {}); } - if config.cancellation_fee_percentage.u64() > 100 { - return Err(ContractError::CancellationFeeTooHigh {}); + if config.burn_fee_rate.u128() > 100 { + return Err(ContractError::BurnFeeTooHigh {}); + } + + if config.queue_size_right <= config.queue_size_left { + return Err(ContractError::QueueSizeRightUnderQueueSizeLeft {}); + } + + if config.duration_days_max > config.duration_days_limit + || config.duration_days_min > config.duration_days_limit + { + return Err(ContractError::DurationDaysLimit {}); } STATE.save(deps.storage, &state)?; CONFIG.save(deps.storage, &config)?; - Ok(Response::new()) + let submsgs = vec![SubMsg { + id: REPLY_ID_INSTANTIATE_SUB_CONTRACTS, + msg: build_instantiate_account_tracker_msg( + config.owner.to_string(), + env.contract.address.to_string(), + msg.account_tracker_code_id.u64(), + ), + gas_limit: None, + reply_on: ReplyOn::Always, + }]; + + Ok(Response::new().add_submessages(submsgs)) } #[cfg_attr(not(feature = "library"), entry_point)] @@ -77,25 +107,39 @@ pub fn execute( info: MessageInfo, msg: ExecuteMsg, ) -> Result { + let config = CONFIG.load(deps.storage)?; match msg { - ExecuteMsg::CreateJob(data) => execute::job::create_job(deps, env, info, data), - ExecuteMsg::DeleteJob(data) => execute::job::delete_job(deps, env, info, data), + ExecuteMsg::CreateJob(data) => execute::job::create_job(deps, env, info, data, config), + ExecuteMsg::DeleteJob(data) => execute::job::delete_job(deps, env, info, data, config), ExecuteMsg::UpdateJob(data) => execute::job::update_job(deps, env, info, data), - ExecuteMsg::ExecuteJob(data) => execute::job::execute_job(deps, env, info, data), - ExecuteMsg::EvictJob(data) => execute::job::evict_job(deps, env, info, data), - - ExecuteMsg::CreateAccount(data) => execute::account::create_account(deps, env, info, data), - - ExecuteMsg::UpdateConfig(data) => execute::controller::update_config(deps, env, info, data), - + ExecuteMsg::ExecuteJob(data) => { + nonpayable(&info).unwrap(); + execute::job::execute_job(deps, env, info, data, config) + } + ExecuteMsg::EvictJob(data) => { + nonpayable(&info).unwrap(); + execute::job::evict_job(deps, env, info, data, config) + } + ExecuteMsg::UpdateConfig(data) => { + nonpayable(&info).unwrap(); + execute::controller::update_config(deps, env, info, data, config) + } ExecuteMsg::MigrateAccounts(data) => { - execute::controller::migrate_accounts(deps, env, info, data) + nonpayable(&info).unwrap(); + migrate::account::migrate_accounts(deps.as_ref(), env, info, data, config) } + ExecuteMsg::MigratePendingJobs(data) => { - execute::controller::migrate_pending_jobs(deps, env, info, data) + nonpayable(&info).unwrap(); + migrate::job::migrate_pending_jobs(deps, env, info, data) } ExecuteMsg::MigrateFinishedJobs(data) => { - execute::controller::migrate_finished_jobs(deps, env, info, data) + nonpayable(&info).unwrap(); + migrate::job::migrate_finished_jobs(deps, env, info, data) + } + + ExecuteMsg::CreateFundingAccount(data) => { + execute::account::create_funding_account(deps, env, info, data) } } } @@ -105,15 +149,10 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { match msg { QueryMsg::QueryJob(data) => to_binary(&query::job::query_job(deps, env, data)?), QueryMsg::QueryJobs(data) => to_binary(&query::job::query_jobs(deps, env, data)?), - - QueryMsg::QueryAccount(data) => to_binary(&query::account::query_account(deps, env, data)?), - QueryMsg::QueryAccounts(data) => { - to_binary(&query::account::query_accounts(deps, env, data)?) - } - QueryMsg::QueryConfig(data) => { to_binary(&query::controller::query_config(deps, env, data)?) } + QueryMsg::QueryState(data) => to_binary(&query::controller::query_state(deps, env, data)?), } } @@ -122,357 +161,25 @@ pub fn migrate(_deps: DepsMut, _env: Env, _msg: MigrateMsg) -> Result Result { - match msg.id { - //account creation - 0 => { - let reply = msg.result.into_result().map_err(StdError::generic_err)?; - - let event = reply - .events - .iter() - .find(|event| { - event - .attributes - .iter() - .any(|attr| attr.key == "action" && attr.value == "instantiate") - }) - .ok_or_else(|| StdError::generic_err("cannot find `instantiate` event"))?; + let config = CONFIG.load(deps.storage)?; - let owner = event - .attributes - .iter() - .cloned() - .find(|attr| attr.key == "owner") - .ok_or_else(|| StdError::generic_err("cannot find `owner` attribute"))? - .value; - - let address = event - .attributes - .iter() - .cloned() - .find(|attr| attr.key == "contract_addr") - .ok_or_else(|| StdError::generic_err("cannot find `contract_addr` attribute"))? - .value; - - let funds: Vec = serde_json_wasm::from_str( - &event - .attributes - .iter() - .cloned() - .find(|attr| attr.key == "funds") - .ok_or_else(|| StdError::generic_err("cannot find `funds` attribute"))? - .value, - )?; - - let cw_funds: Option> = serde_json_wasm::from_str( - &event - .attributes - .iter() - .cloned() - .find(|attr| attr.key == "cw_funds") - .ok_or_else(|| StdError::generic_err("cannot find `cw_funds` attribute"))? - .value, - )?; - - let cw_funds_vec = match cw_funds { - None => { - vec![] - } - Some(funds) => funds, - }; - - let mut msgs_vec: Vec = vec![]; - - for cw_fund in &cw_funds_vec { - msgs_vec.push(CosmosMsg::Wasm(match cw_fund { - Fund::Cw20(cw20_fund) => WasmMsg::Execute { - contract_addr: deps - .api - .addr_validate(&cw20_fund.contract_addr)? - .to_string(), - msg: to_binary(&FundTransferMsgs::TransferFrom(TransferFromMsg { - owner: owner.clone(), - recipient: address.clone(), - amount: cw20_fund.amount, - }))?, - funds: vec![], - }, - Fund::Cw721(cw721_fund) => WasmMsg::Execute { - contract_addr: deps - .api - .addr_validate(&cw721_fund.contract_addr)? - .to_string(), - msg: to_binary(&FundTransferMsgs::TransferNft(TransferNftMsg { - recipient: address.clone(), - token_id: cw721_fund.token_id.clone(), - }))?, - funds: vec![], - }, - })) - } - - if ACCOUNTS().has(deps.storage, deps.api.addr_validate(&owner)?) { - return Err(ContractError::AccountAlreadyExists {}); - } - - ACCOUNTS().save( - deps.storage, - deps.api.addr_validate(&owner)?, - &Account { - owner: deps.api.addr_validate(&owner.clone())?, - account: deps.api.addr_validate(&address)?, - }, - )?; - Ok(Response::new() - .add_attribute("action", "save_account") - .add_attribute("owner", owner) - .add_attribute("account_address", address) - .add_attribute("funds", serde_json_wasm::to_string(&funds)?) - .add_attribute("cw_funds", serde_json_wasm::to_string(&cw_funds_vec)?) - .add_messages(msgs_vec)) + match msg.id { + REPLY_ID_CREATE_JOB_ACCOUNT_AND_JOB => { + reply::account::create_account_and_job(deps, env, msg, config) } - //job execution - _ => { - let mut state = STATE.load(deps.storage)?; - - let new_status = match msg.result { - SubMsgResult::Ok(_) => JobStatus::Executed, - SubMsgResult::Err(_) => JobStatus::Failed, - }; - - let job = PENDING_JOBS().load(deps.storage, msg.id)?; - PENDING_JOBS().remove(deps.storage, msg.id)?; - - let finished_job = FINISHED_JOBS().update(deps.storage, msg.id, |j| match j { - None => Ok(Job { - id: job.id, - owner: job.owner, - last_update_time: job.last_update_time, - name: job.name, - description: job.description, - labels: job.labels, - status: new_status, - condition: job.condition, - terminate_condition: job.terminate_condition, - msgs: job.msgs, - vars: job.vars, - recurring: job.recurring, - requeue_on_evict: job.requeue_on_evict, - reward: job.reward, - assets_to_withdraw: job.assets_to_withdraw, - }), - Some(_) => Err(ContractError::JobAlreadyFinished {}), - })?; - - let res_attrs = match msg.result { - SubMsgResult::Err(e) => vec![Attribute::new( - "transaction_error", - format!("{}. {}", &e, map_contract_error(&e)), - )], - _ => vec![], - }; - - let mut msgs = vec![]; - let mut new_job_attrs = vec![]; - - let account = ACCOUNTS().load(deps.storage, finished_job.owner.clone())?; - let config = CONFIG.load(deps.storage)?; - - //assume reward.amount == warp token allowance - let fee = finished_job.reward * Uint128::from(config.creation_fee_percentage) - / Uint128::new(100); - - let account_amount = deps - .querier - .query::(&QueryRequest::Bank(BankQuery::Balance { - address: account.account.to_string(), - denom: config.fee_denom.clone(), - }))? - .amount - .amount; - - if finished_job.recurring { - if account_amount < fee + finished_job.reward { - new_job_attrs.push(Attribute::new("action", "recur_job")); - new_job_attrs.push(Attribute::new("creation_status", "failed_insufficient_fee")) - } else if !(finished_job.status == JobStatus::Executed - || finished_job.status == JobStatus::Failed) - { - new_job_attrs.push(Attribute::new("action", "recur_job")); - new_job_attrs.push(Attribute::new( - "creation_status", - "failed_invalid_job_status", - )); - } else { - let new_vars: String = deps.querier.query_wasm_smart( - config.resolver_address.clone(), - &resolver::QueryMsg::QueryApplyVarFn(resolver::QueryApplyVarFnMsg { - vars: finished_job.vars, - status: finished_job.status.clone(), - }), - )?; - - let should_terminate_job: bool; - match finished_job.terminate_condition.clone() { - Some(terminate_condition) => { - let resolution: StdResult = deps.querier.query_wasm_smart( - config.resolver_address, - &resolver::QueryMsg::QueryResolveCondition( - resolver::QueryResolveConditionMsg { - condition: terminate_condition, - vars: new_vars.clone(), - }, - ), - ); - if let Err(e) = resolution { - should_terminate_job = true; - new_job_attrs.push(Attribute::new("action", "recur_job")); - new_job_attrs.push(Attribute::new( - "job_terminate_condition_status", - "invalid", - )); - new_job_attrs.push(Attribute::new( - "creation_status", - format!( - "terminated_due_to_terminate_condition_resolves_to_error. {}", - e - ), - )); - } else { - new_job_attrs.push(Attribute::new( - "job_terminate_condition_status", - "valid", - )); - if resolution? { - should_terminate_job = true; - new_job_attrs.push(Attribute::new("action", "recur_job")); - new_job_attrs.push(Attribute::new( - "creation_status", - "terminated_due_to_terminate_condition_resolves_to_true", - )); - } else { - should_terminate_job = false; - } - } - } - None => { - should_terminate_job = false; - } - } - - if !should_terminate_job { - let new_job = PENDING_JOBS().update( - deps.storage, - state.current_job_id.u64(), - |s| match s { - None => Ok(Job { - id: state.current_job_id, - owner: finished_job.owner.clone(), - last_update_time: Uint64::from(env.block.time.seconds()), - name: finished_job.name.clone(), - description: finished_job.description, - labels: finished_job.labels, - status: JobStatus::Pending, - condition: finished_job.condition.clone(), - terminate_condition: finished_job.terminate_condition.clone(), - vars: new_vars, - requeue_on_evict: finished_job.requeue_on_evict, - recurring: finished_job.recurring, - msgs: finished_job.msgs.clone(), - reward: finished_job.reward, - assets_to_withdraw: finished_job.assets_to_withdraw, - }), - Some(_) => Err(ContractError::JobAlreadyExists {}), - }, - )?; - - state.current_job_id = state.current_job_id.checked_add(Uint64::new(1))?; - state.q = state.q.checked_add(Uint64::new(1))?; - - msgs.push( - //send reward to controller - WasmMsg::Execute { - contract_addr: account.account.to_string(), - msg: to_binary(&account::ExecuteMsg::Generic(GenericMsg { - msgs: vec![CosmosMsg::Bank(BankMsg::Send { - to_address: config.fee_collector.to_string(), - amount: vec![Coin::new( - (fee).u128(), - config.fee_denom.clone(), - )], - })], - }))?, - funds: vec![], - }, - ); - - msgs.push( - //send reward to controller - WasmMsg::Execute { - contract_addr: account.account.to_string(), - msg: to_binary(&account::ExecuteMsg::Generic(GenericMsg { - msgs: vec![CosmosMsg::Bank(BankMsg::Send { - to_address: env.contract.address.to_string(), - amount: vec![Coin::new( - (new_job.reward).u128(), - config.fee_denom, - )], - })], - }))?, - funds: vec![], - }, - ); - - msgs.push( - //withdraw all assets that are listed - WasmMsg::Execute { - contract_addr: account.account.to_string(), - msg: to_binary(&account::ExecuteMsg::WithdrawAssets( - WithdrawAssetsMsg { - asset_infos: new_job.assets_to_withdraw, - }, - ))?, - funds: vec![], - }, - ); - - new_job_attrs.push(Attribute::new("action", "create_job")); - new_job_attrs.push(Attribute::new("job_id", new_job.id)); - new_job_attrs.push(Attribute::new("job_owner", new_job.owner)); - new_job_attrs.push(Attribute::new("job_name", new_job.name)); - new_job_attrs.push(Attribute::new( - "job_status", - serde_json_wasm::to_string(&new_job.status)?, - )); - new_job_attrs.push(Attribute::new( - "job_condition", - serde_json_wasm::to_string(&new_job.condition)?, - )); - new_job_attrs.push(Attribute::new( - "job_msgs", - serde_json_wasm::to_string(&new_job.msgs)?, - )); - new_job_attrs.push(Attribute::new("job_reward", new_job.reward)); - new_job_attrs.push(Attribute::new("job_creation_fee", fee)); - new_job_attrs.push(Attribute::new( - "job_last_updated_time", - new_job.last_update_time, - )); - new_job_attrs.push(Attribute::new("sub_action", "recur_job")); - } - } - } - - STATE.save(deps.storage, &state)?; - - Ok(Response::new() - .add_attribute("action", "execute_reply") - .add_attribute("job_id", job.id) - .add_attributes(res_attrs) - .add_attributes(new_job_attrs) - .add_messages(msgs)) + REPLY_ID_CREATE_FUNDING_ACCOUNT => { + reply::account::create_funding_account(deps, env, msg, config) + } + REPLY_ID_INSTANTIATE_SUB_CONTRACTS => { + reply::job::instantiate_sub_contracts(deps, env, msg, config) } + _ => reply::job::execute_job(deps, env, msg, config), } } diff --git a/contracts/warp-controller/src/error.rs b/contracts/warp-controller/src/error.rs index 174fc4f2..3e5f136f 100644 --- a/contracts/warp-controller/src/error.rs +++ b/contracts/warp-controller/src/error.rs @@ -12,6 +12,18 @@ pub enum ContractError { #[error("Unauthorized")] Unauthorized {}, + #[error("Funding account not provided for recurring job")] + FundingAccountMissingForRecurringJob {}, + + #[error("Insufficient funds to pay for reward and fee.")] + InsufficientFundsToPayForRewardAndFee {}, + + #[error("Insufficient operational funds.")] + InsufficientOperationalFunds {}, + + #[error("Insufficient funds to pay for fee.")] + InsufficientFundsToPayForFee {}, + #[error("Funds array in message does not match funds array in job.")] FundsMismatch {}, @@ -24,6 +36,9 @@ pub enum ContractError { #[error("Name cannot exceed 280 characters")] NameTooLong {}, + #[error("Duration days exceeds limit.")] + DurationDaysLimit {}, + #[error("Attempting to distribute more rewards than received from the action")] DistributingMoreRewardThanReceived {}, @@ -36,6 +51,9 @@ pub enum ContractError { #[error("Account already exists")] AccountAlreadyExists {}, + #[error("Job account tracker already exists")] + AccountTrackerAlreadyExists {}, + #[error("Account cannot create an account")] AccountCannotCreateAccount {}, @@ -54,8 +72,8 @@ pub enum ContractError { #[error("Cancellation fee too high")] CancellationFeeTooHigh {}, - #[error("Creation fee too high")] - CreationFeeTooHigh {}, + #[error("Burn fee too high")] + BurnFeeTooHigh {}, #[error("Custom Error val: {val:?}")] CustomError { val: String }, @@ -70,17 +88,23 @@ pub enum ContractError { #[error("Error decoding JSON result")] DecodeError {}, - #[error("Max eviction fee smaller than minimum eviction fee.")] - MaxFeeUnderMinFee {}, + #[error("Creation max fee smaller than minimum fee.")] + CreationMaxFeeUnderMinFee {}, - #[error("Max eviction time smaller than minimum eviction time.")] - MaxTimeUnderMinTime {}, + #[error("Maintenance max fee smaller than minimum fee.")] + MaintenanceMaxFeeUnderMinFee {}, - #[error("Job reward smaller than eviction fee.")] - RewardSmallerThanFee {}, + #[error("Max duration days smaller than minimum duration days.")] + DurationMaxDaysUnderMinDays {}, + + #[error("Queue size right smaller than queue size left.")] + QueueSizeRightUnderQueueSizeLeft {}, #[error("Eviction period not elapsed.")] EvictionPeriodNotElapsed {}, + + #[error("Unknown reply ID.")] + UnknownReplyId {}, } impl From for ContractError { diff --git a/contracts/warp-controller/src/execute/account.rs b/contracts/warp-controller/src/execute/account.rs index 04cdeda8..552e7e7d 100644 --- a/contracts/warp-controller/src/execute/account.rs +++ b/contracts/warp-controller/src/execute/account.rs @@ -1,101 +1,35 @@ -use crate::state::{ACCOUNTS, CONFIG}; -use crate::ContractError; -use controller::account::{ - CreateAccountMsg, Fund, FundTransferMsgs, TransferFromMsg, TransferNftMsg, -}; +use controller::CreateFundingAccountMsg; +use cosmwasm_std::{DepsMut, Env, MessageInfo, ReplyOn, Response, SubMsg, Uint64}; -use cosmwasm_std::{ - to_binary, BankMsg, CosmosMsg, DepsMut, Env, MessageInfo, ReplyOn, Response, SubMsg, WasmMsg, +use crate::{ + contract::REPLY_ID_CREATE_FUNDING_ACCOUNT, state::CONFIG, + util::msg::build_instantiate_warp_account_msg, ContractError, }; -pub fn create_account( +pub fn create_funding_account( deps: DepsMut, env: Env, info: MessageInfo, - data: CreateAccountMsg, + _data: CreateFundingAccountMsg, ) -> Result { let config = CONFIG.load(deps.storage)?; - let item = ACCOUNTS() - .idx - .account - .item(deps.storage, info.sender.clone()); - - if item?.is_some() { - return Err(ContractError::AccountCannotCreateAccount {}); - } - - if ACCOUNTS().has(deps.storage, info.sender.clone()) { - let account = ACCOUNTS().load(deps.storage, info.sender.clone())?; - - let cw_funds_vec = match data.funds { - None => { - vec![] - } - Some(funds) => funds, - }; - - let mut msgs_vec: Vec = vec![]; - - if !info.funds.is_empty() { - msgs_vec.push(CosmosMsg::Bank(BankMsg::Send { - to_address: account.account.to_string(), - amount: info.funds.clone(), - })) - } - - for cw_fund in &cw_funds_vec { - msgs_vec.push(CosmosMsg::Wasm(match cw_fund { - Fund::Cw20(cw20_fund) => WasmMsg::Execute { - contract_addr: deps - .api - .addr_validate(&cw20_fund.contract_addr)? - .to_string(), - msg: to_binary(&FundTransferMsgs::TransferFrom(TransferFromMsg { - owner: info.sender.clone().to_string(), - recipient: account.account.clone().to_string(), - amount: cw20_fund.amount, - }))?, - funds: vec![], - }, - Fund::Cw721(cw721_fund) => WasmMsg::Execute { - contract_addr: deps - .api - .addr_validate(&cw721_fund.contract_addr)? - .to_string(), - msg: to_binary(&FundTransferMsgs::TransferNft(TransferNftMsg { - recipient: account.account.clone().to_string(), - token_id: cw721_fund.token_id.clone(), - }))?, - funds: vec![], - }, - })) - } - - return Ok(Response::new() - .add_attribute("action", "create_account") - .add_attribute("owner", account.owner) - .add_attribute("account_address", account.account) - .add_messages(msgs_vec)); - } - - let submsg = SubMsg { - id: 0, - msg: CosmosMsg::Wasm(WasmMsg::Instantiate { - admin: Some(env.contract.address.to_string()), - code_id: config.warp_account_code_id.u64(), - msg: to_binary(&account::InstantiateMsg { - owner: info.sender.to_string(), - funds: data.funds, - })?, - funds: info.funds, - label: info.sender.to_string(), - }), + let submsgs = vec![SubMsg { + id: REPLY_ID_CREATE_FUNDING_ACCOUNT, + msg: build_instantiate_warp_account_msg( + Uint64::from(0u64), // placeholder + env.contract.address.to_string(), + config.warp_account_code_id.u64(), + info.sender.to_string(), + info.funds, + None, + None, + ), gas_limit: None, reply_on: ReplyOn::Always, - }; + }]; Ok(Response::new() - .add_attribute("action", "create_account") - .add_submessage(submsg)) + .add_attribute("action", "create_funding_account") + .add_submessages(submsgs)) } diff --git a/contracts/warp-controller/src/execute/controller.rs b/contracts/warp-controller/src/execute/controller.rs index a7076f3b..54eeef56 100644 --- a/contracts/warp-controller/src/execute/controller.rs +++ b/contracts/warp-controller/src/execute/controller.rs @@ -1,17 +1,16 @@ -use crate::state::{ACCOUNTS, CONFIG, FINISHED_JOBS, PENDING_JOBS}; -use crate::ContractError; -use controller::{MigrateAccountsMsg, MigrateJobsMsg, UpdateConfigMsg}; +use cosmwasm_std::{DepsMut, Env, MessageInfo, Response}; -use cosmwasm_std::{to_binary, DepsMut, Env, MessageInfo, Order, Response, WasmMsg}; -use cw_storage_plus::Bound; +use crate::{state::CONFIG, ContractError}; + +use controller::{Config, UpdateConfigMsg}; pub fn update_config( deps: DepsMut, _env: Env, info: MessageInfo, data: UpdateConfigMsg, + mut config: Config, ) -> Result { - let mut config = CONFIG.load(deps.storage)?; if info.sender != config.owner { return Err(ContractError::Unauthorized {}); } @@ -26,37 +25,53 @@ pub fn update_config( Some(data) => deps.api.addr_validate(data.as_str())?, }; config.minimum_reward = data.minimum_reward.unwrap_or(config.minimum_reward); - config.creation_fee_percentage = data - .creation_fee_percentage - .unwrap_or(config.creation_fee_percentage); - config.cancellation_fee_percentage = data - .cancellation_fee_percentage - .unwrap_or(config.cancellation_fee_percentage); + config.cancellation_fee_rate = data + .cancellation_fee_rate + .unwrap_or(config.cancellation_fee_rate); + + config.creation_fee_min = data.creation_fee_min.unwrap_or(config.creation_fee_min); + config.creation_fee_max = data.creation_fee_max.unwrap_or(config.creation_fee_max); + config.burn_fee_min = data.burn_fee_min.unwrap_or(config.burn_fee_min); + config.maintenance_fee_min = data + .maintenance_fee_min + .unwrap_or(config.maintenance_fee_min); + config.maintenance_fee_max = data + .maintenance_fee_max + .unwrap_or(config.maintenance_fee_max); + config.duration_days_min = data.duration_days_min.unwrap_or(config.duration_days_min); + config.duration_days_max = data.duration_days_max.unwrap_or(config.duration_days_max); + config.queue_size_left = data.queue_size_left.unwrap_or(config.queue_size_left); + config.queue_size_right = data.queue_size_right.unwrap_or(config.queue_size_right); + config.burn_fee_rate = data.burn_fee_rate.unwrap_or(config.burn_fee_rate); + + if config.burn_fee_rate.u128() > 100 { + return Err(ContractError::BurnFeeTooHigh {}); + } - config.a_max = data.a_max.unwrap_or(config.a_max); - config.a_min = data.a_min.unwrap_or(config.a_min); - config.t_max = data.t_max.unwrap_or(config.t_max); - config.t_min = data.t_min.unwrap_or(config.t_min); - config.q_max = data.q_max.unwrap_or(config.q_max); + if config.creation_fee_max < config.creation_fee_min { + return Err(ContractError::CreationMaxFeeUnderMinFee {}); + } - if config.a_max < config.a_min { - return Err(ContractError::MaxFeeUnderMinFee {}); + if config.maintenance_fee_max < config.maintenance_fee_min { + return Err(ContractError::MaintenanceMaxFeeUnderMinFee {}); } - if config.t_max < config.t_min { - return Err(ContractError::MaxTimeUnderMinTime {}); + if config.duration_days_max <= config.duration_days_min { + return Err(ContractError::DurationMaxDaysUnderMinDays {}); } - if config.minimum_reward < config.a_min { - return Err(ContractError::RewardSmallerThanFee {}); + if config.cancellation_fee_rate.u64() > 100 { + return Err(ContractError::CancellationFeeTooHigh {}); } - if config.creation_fee_percentage.u64() > 100 { - return Err(ContractError::CreationFeeTooHigh {}); + if config.queue_size_right <= config.queue_size_left { + return Err(ContractError::QueueSizeRightUnderQueueSizeLeft {}); } - if config.cancellation_fee_percentage.u64() > 100 { - return Err(ContractError::CancellationFeeTooHigh {}); + if config.duration_days_max > config.duration_days_limit + || config.duration_days_min > config.duration_days_limit + { + return Err(ContractError::DurationDaysLimit {}); } CONFIG.save(deps.storage, &config)?; @@ -66,109 +81,5 @@ pub fn update_config( .add_attribute("config_owner", config.owner) .add_attribute("config_fee_collector", config.fee_collector) .add_attribute("config_minimum_reward", config.minimum_reward) - .add_attribute( - "config_creation_fee_percentage", - config.creation_fee_percentage, - ) - .add_attribute( - "config_cancellation_fee_percentage", - config.cancellation_fee_percentage, - ) - .add_attribute("config_a_max", config.a_max) - .add_attribute("config_a_min", config.a_min) - .add_attribute("config_t_max", config.t_max) - .add_attribute("config_t_min", config.t_min) - .add_attribute("config_q_max", config.q_max)) -} - -pub fn migrate_accounts( - deps: DepsMut, - _env: Env, - info: MessageInfo, - msg: MigrateAccountsMsg, -) -> Result { - let config = CONFIG.load(deps.storage)?; - if info.sender != config.owner { - return Err(ContractError::Unauthorized {}); - } - - let start_after = match msg.start_after { - None => None, - Some(s) => Some(deps.api.addr_validate(s.as_str())?), - }; - let start_after = start_after.map(Bound::exclusive); - - let account_keys: Result, _> = ACCOUNTS() - .keys(deps.storage, start_after, None, Order::Ascending) - .take(msg.limit as usize) - .collect(); - let account_keys = account_keys?; - let mut migration_msgs = vec![]; - - for account_key in account_keys { - let account_address = ACCOUNTS().load(deps.storage, account_key)?.account; - migration_msgs.push(WasmMsg::Migrate { - contract_addr: account_address.to_string(), - new_code_id: msg.warp_account_code_id.u64(), - msg: to_binary(&account::MigrateMsg {})?, - }) - } - - Ok(Response::new().add_messages(migration_msgs)) -} - -pub fn migrate_pending_jobs( - deps: DepsMut, - _env: Env, - info: MessageInfo, - msg: MigrateJobsMsg, -) -> Result { - let config = CONFIG.load(deps.storage)?; - if info.sender != config.owner { - return Err(ContractError::Unauthorized {}); - } - - let start_after = msg.start_after; - let start_after = start_after.map(Bound::exclusive); - - let job_keys: Result, _> = PENDING_JOBS() - .keys(deps.storage, start_after, None, Order::Ascending) - .take(msg.limit as usize) - .collect(); - let job_keys = job_keys?; - for job_key in job_keys.clone() { - let v1_job = PENDING_JOBS().load(deps.storage, job_key)?; - - PENDING_JOBS().save(deps.storage, job_key, &v1_job)?; - } - - Ok(Response::new().add_attribute("refreshed_finished_jobs", job_keys.len().to_string())) -} - -pub fn migrate_finished_jobs( - deps: DepsMut, - _env: Env, - info: MessageInfo, - msg: MigrateJobsMsg, -) -> Result { - let config = CONFIG.load(deps.storage)?; - if info.sender != config.owner { - return Err(ContractError::Unauthorized {}); - } - - let start_after = msg.start_after; - let start_after = start_after.map(Bound::exclusive); - - let job_keys: Result, _> = FINISHED_JOBS() - .keys(deps.storage, start_after, None, Order::Ascending) - .take(msg.limit as usize) - .collect(); - let job_keys = job_keys?; - for job_key in job_keys.clone() { - let v1_job = FINISHED_JOBS().load(deps.storage, job_key)?; - - FINISHED_JOBS().save(deps.storage, job_key, &v1_job)?; - } - - Ok(Response::new().add_attribute("refreshed_finished_jobs", job_keys.len().to_string())) + .add_attribute("config_cancellation_fee_rate", config.cancellation_fee_rate)) } diff --git a/contracts/warp-controller/src/execute/fee.rs b/contracts/warp-controller/src/execute/fee.rs new file mode 100644 index 00000000..a9677d09 --- /dev/null +++ b/contracts/warp-controller/src/execute/fee.rs @@ -0,0 +1,49 @@ +use controller::Config; +use cosmwasm_std::{Uint128, Uint64}; + +pub fn compute_creation_fee(queue_size: Uint64, config: &Config) -> Uint128 { + let x1 = Uint128::from(config.queue_size_left); + let y1 = config.creation_fee_min; + let x2 = Uint128::from(config.queue_size_right); + let y2 = config.creation_fee_max; + let qs = Uint128::from(queue_size); + + let slope = (y2 - y1) / (x2 - x1); + + if qs < x1 { + config.creation_fee_min + } else if qs < x2 { + slope * qs + y1 - slope * x1 + } else { + config.creation_fee_max + } +} + +pub fn compute_maintenance_fee(duration_days: Uint64, config: &Config) -> Uint128 { + let x1 = Uint128::from(config.duration_days_min); + let y1 = config.maintenance_fee_min; + let x2 = Uint128::from(config.duration_days_max); + let y2 = config.maintenance_fee_max; + let dd = Uint128::from(duration_days); + + let slope = (y2 - y1) / (x2 - x1); + + if dd < x1 { + config.maintenance_fee_min + } else if dd < x2 { + slope * dd + y1 - slope * x1 + } else { + config.maintenance_fee_max + } +} + +pub fn compute_burn_fee(job_reward: Uint128, config: &Config) -> Uint128 { + let min_fee: Uint128 = config.burn_fee_min; + let calculated_fee = job_reward * config.burn_fee_rate / Uint128::new(100); + + if calculated_fee > min_fee { + calculated_fee + } else { + min_fee + } +} diff --git a/contracts/warp-controller/src/execute/job.rs b/contracts/warp-controller/src/execute/job.rs index fe104dda..36963c83 100644 --- a/contracts/warp-controller/src/execute/job.rs +++ b/contracts/warp-controller/src/execute/job.rs @@ -1,17 +1,34 @@ -use crate::state::{ACCOUNTS, CONFIG, FINISHED_JOBS, PENDING_JOBS, STATE}; +use crate::contract::REPLY_ID_CREATE_JOB_ACCOUNT_AND_JOB; +use crate::state::{JobQueue, STATE}; +use crate::util::msg::{ + build_account_execute_generic_msgs, build_account_execute_warp_msgs, + build_free_funding_account_msg, build_take_funding_account_msg, +}; use crate::ContractError; -use crate::ContractError::EvictionPeriodNotElapsed; -use account::GenericMsg; +use controller::account::WarpMsgs; use controller::job::{ - CreateJobMsg, DeleteJobMsg, EvictJobMsg, ExecuteJobMsg, Job, JobStatus, UpdateJobMsg, + CreateJobMsg, DeleteJobMsg, EvictJobMsg, ExecuteJobMsg, Execution, Job, JobStatus, UpdateJobMsg, }; -use controller::State; use cosmwasm_std::{ - to_binary, Attribute, BalanceResponse, BankMsg, BankQuery, Coin, CosmosMsg, DepsMut, Env, - MessageInfo, QueryRequest, ReplyOn, Response, StdResult, SubMsg, Uint128, Uint64, WasmMsg, + to_binary, Attribute, Coin, CosmosMsg, DepsMut, Env, MessageInfo, ReplyOn, Response, StdResult, + SubMsg, Uint128, Uint64, WasmMsg, +}; + +use crate::util::{ + fee::deduct_from_native_funds, + msg::{ + build_account_withdraw_assets_msg, build_free_job_account_msg, + build_instantiate_warp_account_msg, build_take_job_account_msg, build_transfer_cw20_msg, + build_transfer_cw721_msg, build_transfer_native_funds_msg, + }, }; + +use account_tracker::{FundingAccount, FundingAccountResponse, JobAccountResponse}; +use controller::{account::CwFund, Config}; use resolver::QueryHydrateMsgsMsg; +use super::fee::{compute_burn_fee, compute_creation_fee, compute_maintenance_fee}; + const MAX_TEXT_LENGTH: usize = 280; pub fn create_job( @@ -19,10 +36,8 @@ pub fn create_job( env: Env, info: MessageInfo, data: CreateJobMsg, + config: Config, ) -> Result { - let state = STATE.load(deps.storage)?; - let config = CONFIG.load(deps.storage)?; - if data.name.len() > MAX_TEXT_LENGTH { return Err(ContractError::NameTooLong {}); } @@ -35,107 +50,309 @@ pub fn create_job( return Err(ContractError::RewardTooSmall {}); } + if data.duration_days > config.duration_days_limit { + return Err(ContractError::DurationDaysLimit {}); + } + + let state = STATE.load(deps.storage)?; + + let job_owner = info.sender.clone(); + let account_tracker_address_ref = &config.account_tracker_address.to_string(); + let _validate_conditions_and_variables: Option = deps.querier.query_wasm_smart( - config.resolver_address, + &config.resolver_address, &resolver::QueryMsg::QueryValidateJobCreation(resolver::QueryValidateJobCreationMsg { - condition: data.condition.clone(), terminate_condition: data.terminate_condition.clone(), vars: data.vars.clone(), - msgs: data.msgs.clone(), + executions: data.executions.clone(), }), )?; - let account_record = ACCOUNTS() - .idx - .account - .item(deps.storage, info.sender.clone())?; + let creation_fee = compute_creation_fee(state.q, &config); + let maintenance_fee = compute_maintenance_fee(data.duration_days, &config); + let burn_fee = compute_burn_fee(data.reward, &config); + + let total_fees = creation_fee + maintenance_fee + burn_fee; + + if data.funding_account.is_none() && data.recurring { + return Err(ContractError::FundingAccountMissingForRecurringJob {}); + } + + if data.funding_account.is_none() { + if data.operational_amount < total_fees + data.reward { + return Err(ContractError::InsufficientOperationalFunds {}); + } + + let fee_denom_paid_amount = info + .funds + .iter() + .find(|f| f.denom == config.fee_denom) + .unwrap() + .amount; + + if fee_denom_paid_amount < data.operational_amount { + return Err(ContractError::InsufficientFundsToPayForRewardAndFee {}); + } + } - let account = match account_record { - None => ACCOUNTS() - .load(deps.storage, info.sender) - .map_err(|_e| ContractError::AccountDoesNotExist {})?, - Some(record) => record.1, + // ignore operational_amount when funding_account is provided + let operational_amount = if data.funding_account.is_some() { + Uint128::zero() + } else { + data.operational_amount }; - let job = PENDING_JOBS().update(deps.storage, state.current_job_id.u64(), |s| match s { - None => Ok(Job { + // Reward and fee will always be in native denom + let native_funds_minus_operational_amount = deduct_from_native_funds( + info.funds.clone(), + config.fee_denom.clone(), + operational_amount, + ); + + let mut submsgs = vec![]; + let mut msgs = vec![]; + let mut attrs = vec![]; + + let mut job = JobQueue::add( + deps.storage, + Job { id: state.current_job_id, - owner: account.owner, + prev_id: None, + owner: job_owner.clone(), + // Account uses a placeholder value for now, will update it to job account address if job account exists or after created + // Update will happen either in create_job (exists free job account) or reply (after creation), so it's atomic + // And we guarantee we do not read this value before it's updated + account: info.sender.clone(), last_update_time: Uint64::from(env.block.time.seconds()), name: data.name, status: JobStatus::Pending, - condition: data.condition.clone(), terminate_condition: data.terminate_condition, recurring: data.recurring, - requeue_on_evict: data.requeue_on_evict, vars: data.vars, - msgs: data.msgs, + executions: data.executions, reward: data.reward, description: data.description, labels: data.labels, assets_to_withdraw: data.assets_to_withdraw.unwrap_or(vec![]), - }), - Some(_) => Err(ContractError::JobAlreadyExists {}), - })?; - - STATE.save( - deps.storage, - &State { - current_job_id: state.current_job_id.checked_add(Uint64::new(1))?, - q: state.q.checked_add(Uint64::new(1))?, + duration_days: data.duration_days, + created_at_time: Uint64::from(env.block.time.seconds()), + // placeholder, will be updated later on + funding_account: None, }, )?; - //assume reward.amount == warp token allowance - let fee = data.reward * Uint128::from(config.creation_fee_percentage) / Uint128::new(100); - - let reward_send_msgs = vec![ - //send reward to controller - WasmMsg::Execute { - contract_addr: account.account.to_string(), - msg: to_binary(&account::ExecuteMsg::Generic(GenericMsg { - msgs: vec![CosmosMsg::Bank(BankMsg::Send { - to_address: env.contract.address.to_string(), - amount: vec![Coin::new((data.reward).u128(), config.fee_denom.clone())], - })], - }))?, - funds: vec![], - }, - WasmMsg::Execute { - contract_addr: account.account.to_string(), - msg: to_binary(&account::ExecuteMsg::Generic(GenericMsg { - msgs: vec![CosmosMsg::Bank(BankMsg::Send { - to_address: config.fee_collector.to_string(), - amount: vec![Coin::new((fee).u128(), config.fee_denom)], - })], - }))?, - funds: vec![], - }, - ]; + let job_account_resp: JobAccountResponse = deps.querier.query_wasm_smart( + account_tracker_address_ref, + &account_tracker::QueryMsg::QueryFirstFreeJobAccount( + account_tracker::QueryFirstFreeJobAccountMsg { + account_owner_addr: job_owner.to_string(), + }, + ), + )?; + + match job_account_resp.job_account { + None => { + // Create account then create job in reply + submsgs.push(SubMsg { + id: REPLY_ID_CREATE_JOB_ACCOUNT_AND_JOB, + msg: build_instantiate_warp_account_msg( + job.id, + env.contract.address.to_string(), + config.warp_account_code_id.u64(), + info.sender.to_string(), + native_funds_minus_operational_amount, + data.cw_funds, + data.account_msgs, + ), + gas_limit: None, + reply_on: ReplyOn::Always, + }); + + attrs.push(Attribute::new("action", "create_account_and_job")); + } + Some(available_account) => { + let available_account_addr = &available_account.account_addr; + // Update job.account from placeholder value to job account + job.account = available_account_addr.clone(); + JobQueue::sync(deps.storage, env.clone(), job.clone())?; + + if !native_funds_minus_operational_amount.is_empty() { + // Fund account in native coins + msgs.push(build_transfer_native_funds_msg( + available_account_addr.to_string(), + native_funds_minus_operational_amount, + )) + } + + if let Some(cw_funds) = data.cw_funds { + // Fund account in CW20 / CW721 tokens + for cw_fund in cw_funds { + msgs.push(match cw_fund { + CwFund::Cw20(cw20_fund) => build_transfer_cw20_msg( + deps.api + .addr_validate(&cw20_fund.contract_addr)? + .to_string(), + info.sender.clone().to_string(), + available_account_addr.clone().to_string(), + cw20_fund.amount, + ), + CwFund::Cw721(cw721_fund) => build_transfer_cw721_msg( + deps.api + .addr_validate(&cw721_fund.contract_addr)? + .to_string(), + available_account_addr.clone().to_string(), + cw721_fund.token_id.clone(), + ), + }) + } + } + + if let Some(account_msgs) = data.account_msgs { + // Account execute msgs + msgs.push(build_account_execute_warp_msgs( + available_account_addr.to_string(), + account_msgs, + )); + } + + // Take account + msgs.push(build_take_job_account_msg( + config.account_tracker_address.to_string(), + job_owner.to_string(), + available_account_addr.to_string(), + job.id, + )); + + attrs.push(Attribute::new("action", "create_job")); + attrs.push(Attribute::new("job_id", job.id)); + attrs.push(Attribute::new("job_owner", job.owner.clone())); + attrs.push(Attribute::new("job_name", job.name.clone())); + attrs.push(Attribute::new( + "job_status", + serde_json_wasm::to_string(&job.status)?, + )); + attrs.push(Attribute::new( + "job_executions", + serde_json_wasm::to_string(&job.executions)?, + )); + attrs.push(Attribute::new("job_reward", job.reward)); + attrs.push(Attribute::new("job_creation_fee", creation_fee.to_string())); + attrs.push(Attribute::new( + "job_maintenance_fee", + maintenance_fee.to_string(), + )); + attrs.push(Attribute::new("job_burn_fee", burn_fee.to_string())); + attrs.push(Attribute::new("job_total_fees", total_fees.to_string())); + attrs.push(Attribute::new( + "job_last_updated_time", + job.last_update_time, + )); + } + } + + let mut funding_account: Option = None; + + if let Some(funding_account_addr) = data.funding_account { + // fetch funding account and check if it exists, throw otherwise + let funding_account_resp: FundingAccountResponse = deps.querier.query_wasm_smart( + account_tracker_address_ref, + &account_tracker::QueryMsg::QueryFundingAccount( + account_tracker::QueryFundingAccountMsg { + account_addr: funding_account_addr.to_string(), + account_owner_addr: info.sender.to_string(), + }, + ), + )?; + + funding_account = funding_account_resp.funding_account; + } + + match funding_account { + None => { + // exit only applies for recurring jobs, otherwise funds are in controller + if data.recurring { + return Err(ContractError::FundingAccountMissingForRecurringJob {}); + } + } + Some(available_account) => { + let available_account_addr = &available_account.account_addr; + // Update funding_account from placeholder value to funding account + job.funding_account = Some(available_account_addr.clone()); + JobQueue::sync(deps.storage, env.clone(), job.clone())?; + + // transfer reward + fees to controller from funding account + msgs.push(build_account_execute_generic_msgs( + job.funding_account.clone().unwrap().to_string(), + vec![build_transfer_native_funds_msg( + env.contract.address.to_string(), + vec![Coin::new( + total_fees.u128() + data.reward.u128(), + config.fee_denom.clone(), + )], + )], + )); + + // Take account + msgs.push(build_take_funding_account_msg( + config.account_tracker_address.to_string(), + job_owner.to_string(), + available_account_addr.to_string(), + job.id, + )); + + attrs.push(Attribute::new("action", "create_job")); + attrs.push(Attribute::new("job_id", job.id)); + attrs.push(Attribute::new("job_owner", job.owner)); + attrs.push(Attribute::new("job_name", job.name)); + attrs.push(Attribute::new( + "job_status", + serde_json_wasm::to_string(&job.status)?, + )); + attrs.push(Attribute::new( + "job_executions", + serde_json_wasm::to_string(&job.executions)?, + )); + attrs.push(Attribute::new("job_reward", job.reward)); + attrs.push(Attribute::new("job_creation_fee", creation_fee.to_string())); + attrs.push(Attribute::new( + "job_maintenance_fee", + maintenance_fee.to_string(), + )); + attrs.push(Attribute::new("job_burn_fee", burn_fee.to_string())); + attrs.push(Attribute::new("job_total_fees", total_fees.to_string())); + attrs.push(Attribute::new( + "job_last_updated_time", + job.last_update_time, + )); + } + } + + // Job owner sends reward to controller when it calls create_job + // Reward stays at controller, no need to send it elsewhere + msgs.push( + // Job owner sends fee to controller when it calls create_job + // Controller sends fee to fee collector + build_transfer_native_funds_msg( + config.fee_collector.to_string(), + vec![Coin::new(total_fees.u128(), config.fee_denom)], + ), + ); Ok(Response::new() - .add_messages(reward_send_msgs) - .add_attribute("action", "create_job") - .add_attribute("job_id", job.id) - .add_attribute("job_owner", job.owner) - .add_attribute("job_name", job.name) - .add_attribute("job_status", serde_json_wasm::to_string(&job.status)?) - .add_attribute("job_condition", serde_json_wasm::to_string(&job.condition)?) - .add_attribute("job_msgs", serde_json_wasm::to_string(&job.msgs)?) - .add_attribute("job_reward", job.reward) - .add_attribute("job_creation_fee", fee) - .add_attribute("job_last_updated_time", job.last_update_time)) + .add_submessages(submsgs) + .add_messages(msgs) + .add_attributes(attrs)) } pub fn delete_job( deps: DepsMut, - _env: Env, + env: Env, info: MessageInfo, data: DeleteJobMsg, + config: Config, ) -> Result { - let config = CONFIG.load(deps.storage)?; - let state = STATE.load(deps.storage)?; - let job = PENDING_JOBS().load(deps.storage, data.id.u64())?; + let job = JobQueue::get(deps.storage, data.id.into())?; + let account_addr = job.account.clone(); if job.status != JobStatus::Pending { return Err(ContractError::JobNotActive {}); @@ -145,57 +362,53 @@ pub fn delete_job( return Err(ContractError::Unauthorized {}); } - let account = ACCOUNTS().load(deps.storage, info.sender)?; - - PENDING_JOBS().remove(deps.storage, data.id.u64())?; - let _new_job = FINISHED_JOBS().update(deps.storage, data.id.u64(), |h| match h { - None => Ok(Job { - id: job.id, - owner: job.owner, - last_update_time: job.last_update_time, - name: job.name, - status: JobStatus::Cancelled, - condition: job.condition, - terminate_condition: job.terminate_condition, - msgs: job.msgs, - vars: job.vars, - recurring: job.recurring, - requeue_on_evict: job.requeue_on_evict, - reward: job.reward, - description: job.description, - labels: job.labels, - assets_to_withdraw: job.assets_to_withdraw, - }), - Some(_job) => Err(ContractError::JobAlreadyFinished {}), - })?; - - STATE.save( - deps.storage, - &State { - current_job_id: state.current_job_id, - q: state.q.checked_sub(Uint64::new(1))?, - }, - )?; + let _new_job = JobQueue::finalize(deps.storage, env, job.id.into(), JobStatus::Cancelled)?; + + let fee = job.reward * Uint128::from(config.cancellation_fee_rate) / Uint128::new(100); + + let mut msgs = vec![]; + + // Controller sends reward minus cancellation fee back to job owner + msgs.push(build_transfer_native_funds_msg( + job.owner.to_string(), + vec![Coin::new( + (job.reward - fee).u128(), + config.fee_denom.clone(), + )], + )); + + // Job owner sends fee to controller when it calls delete_job + // Controller sends cancellation fee to fee collector + msgs.push(build_transfer_native_funds_msg( + config.fee_collector.to_string(), + vec![Coin::new(fee.u128(), config.fee_denom.clone())], + )); + + // Free account + msgs.push(build_free_job_account_msg( + config.account_tracker_address.to_string(), + job.owner.to_string(), + account_addr.to_string(), + job.id, + )); + + if let Some(funding_account) = job.funding_account { + msgs.push(build_free_funding_account_msg( + config.account_tracker_address.to_string(), + job.owner.to_string(), + funding_account.to_string(), + job.id, + )); + } - let fee = job.reward * Uint128::from(config.cancellation_fee_percentage) / Uint128::new(100); - - let cw20_send_msgs = vec![ - //send reward minus fee back to account - BankMsg::Send { - to_address: account.account.to_string(), - amount: vec![Coin::new( - (job.reward - fee).u128(), - config.fee_denom.clone(), - )], - }, - BankMsg::Send { - to_address: config.fee_collector.to_string(), - amount: vec![Coin::new(fee.u128(), config.fee_denom)], - }, - ]; + // Job owner withdraw all assets that are listed from warp account to itself + msgs.push(build_account_withdraw_assets_msg( + account_addr.to_string(), + job.assets_to_withdraw, + )); Ok(Response::new() - .add_messages(cw20_send_msgs) + .add_messages(msgs) .add_attribute("action", "delete_job") .add_attribute("job_id", job.id) .add_attribute("job_status", serde_json_wasm::to_string(&job.status)?) @@ -208,17 +421,12 @@ pub fn update_job( info: MessageInfo, data: UpdateJobMsg, ) -> Result { - let job = PENDING_JOBS().load(deps.storage, data.id.u64())?; - let config = CONFIG.load(deps.storage)?; + let job = JobQueue::get(deps.storage, data.id.into())?; if info.sender != job.owner { return Err(ContractError::Unauthorized {}); } - let account = ACCOUNTS().load(deps.storage, info.sender)?; - - let added_reward = data.added_reward.unwrap_or(Uint128::new(0)); - if data.name.is_some() && data.name.clone().unwrap().len() > MAX_TEXT_LENGTH { return Err(ContractError::NameTooLong {}); } @@ -227,99 +435,31 @@ pub fn update_job( return Err(ContractError::NameTooShort {}); } - let job = PENDING_JOBS().update(deps.storage, data.id.u64(), |h| match h { - None => Err(ContractError::JobDoesNotExist {}), - Some(job) => Ok(Job { - id: job.id, - owner: job.owner, - last_update_time: if added_reward > config.minimum_reward { - Uint64::new(env.block.time.seconds()) - } else { - job.last_update_time - }, - name: data.name.unwrap_or(job.name), - description: data.description.unwrap_or(job.description), - labels: data.labels.unwrap_or(job.labels), - status: job.status, - condition: job.condition, - terminate_condition: job.terminate_condition, - msgs: job.msgs, - vars: job.vars, - recurring: job.recurring, - requeue_on_evict: job.requeue_on_evict, - reward: job.reward + added_reward, - assets_to_withdraw: job.assets_to_withdraw, - }), - })?; - - let fee = added_reward * Uint128::from(config.creation_fee_percentage) / Uint128::new(100); - - if !added_reward.is_zero() && fee.is_zero() { - return Err(ContractError::RewardTooSmall {}); - } - - let mut cw20_send_msgs = vec![]; - - if added_reward.u128() > 0 { - cw20_send_msgs.push( - //send reward to controller - WasmMsg::Execute { - contract_addr: account.account.to_string(), - msg: to_binary(&account::ExecuteMsg::Generic(GenericMsg { - msgs: vec![CosmosMsg::Bank(BankMsg::Send { - to_address: env.contract.address.to_string(), - amount: vec![Coin::new((added_reward).u128(), config.fee_denom.clone())], - })], - }))?, - funds: vec![], - }, - ); - cw20_send_msgs.push( - //send reward to controller - WasmMsg::Execute { - contract_addr: account.account.to_string(), - msg: to_binary(&account::ExecuteMsg::Generic(GenericMsg { - msgs: vec![CosmosMsg::Bank(BankMsg::Send { - to_address: config.fee_collector.to_string(), - amount: vec![Coin::new((fee).u128(), config.fee_denom)], - })], - }))?, - funds: vec![], - }, - ); - } + let job = JobQueue::update(deps.storage, env, data)?; Ok(Response::new() - .add_messages(cw20_send_msgs) .add_attribute("action", "update_job") .add_attribute("job_id", job.id) .add_attribute("job_owner", job.owner) .add_attribute("job_name", job.name) .add_attribute("job_status", serde_json_wasm::to_string(&job.status)?) - .add_attribute("job_condition", serde_json_wasm::to_string(&job.condition)?) - .add_attribute("job_msgs", serde_json_wasm::to_string(&job.msgs)?) + .add_attribute( + "job_executions", + serde_json_wasm::to_string(&job.executions)?, + ) .add_attribute("job_reward", job.reward) - .add_attribute("job_update_fee", fee) .add_attribute("job_last_updated_time", job.last_update_time)) } pub fn execute_job( deps: DepsMut, - _env: Env, + env: Env, info: MessageInfo, data: ExecuteJobMsg, + config: Config, ) -> Result { - let _config = CONFIG.load(deps.storage)?; - let state = STATE.load(deps.storage)?; - let config = CONFIG.load(deps.storage)?; - let job = PENDING_JOBS().load(deps.storage, data.id.u64())?; - let account = ACCOUNTS().load(deps.storage, job.owner.clone())?; - - if !ACCOUNTS().has(deps.storage, info.sender.clone()) { - return Err(ContractError::AccountDoesNotExist {}); - } - - let keeper_account = ACCOUNTS().load(deps.storage, info.sender.clone())?; + let job = JobQueue::get(deps.storage, data.id.into())?; + let account_addr = job.account.clone(); if job.status != JobStatus::Pending { return Err(ContractError::JobNotActive {}); @@ -330,87 +470,86 @@ pub fn execute_job( &resolver::QueryMsg::QueryHydrateVars(resolver::QueryHydrateVarsMsg { vars: job.vars, external_inputs: data.external_inputs, + warp_account_addr: Some(job.account.to_string()), }), )?; - let resolution: StdResult = deps.querier.query_wasm_smart( - config.resolver_address.clone(), - &resolver::QueryMsg::QueryResolveCondition(resolver::QueryResolveConditionMsg { - condition: job.condition, - vars: vars.clone(), - }), - ); - let mut attrs = vec![]; + let mut msgs = vec![]; let mut submsgs = vec![]; - if let Err(e) = resolution { - attrs.push(Attribute::new("job_condition_status", "invalid")); - attrs.push(Attribute::new("error", e.to_string())); - let job = PENDING_JOBS().load(deps.storage, data.id.u64())?; - FINISHED_JOBS().save( - deps.storage, - data.id.u64(), - &Job { - id: job.id, - owner: job.owner, - last_update_time: job.last_update_time, - name: job.name, - description: job.description, - labels: job.labels, - status: JobStatus::Failed, - condition: job.condition, - terminate_condition: job.terminate_condition, - msgs: job.msgs, - vars, - recurring: job.recurring, - requeue_on_evict: job.requeue_on_evict, - reward: job.reward, - assets_to_withdraw: job.assets_to_withdraw, - }, - )?; - PENDING_JOBS().remove(deps.storage, data.id.u64())?; - STATE.save( - deps.storage, - &State { - current_job_id: state.current_job_id, - q: state.q.checked_sub(Uint64::new(1))?, - }, - )?; - } else { - attrs.push(Attribute::new("job_condition_status", "valid")); - if !resolution? { - return Err(ContractError::JobNotActive {}); - } - - submsgs.push(SubMsg { - id: job.id.u64(), - msg: CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: account.account.to_string(), - msg: to_binary(&account::ExecuteMsg::Generic(GenericMsg { - msgs: deps.querier.query_wasm_smart( - config.resolver_address, - &resolver::QueryMsg::QueryHydrateMsgs(QueryHydrateMsgsMsg { - msgs: job.msgs, - vars, - }), - )?, - }))?, - funds: vec![], + for Execution { condition, msgs } in job.executions { + let resolution: StdResult = deps.querier.query_wasm_smart( + config.resolver_address.clone(), + &resolver::QueryMsg::QueryResolveCondition(resolver::QueryResolveConditionMsg { + condition, + vars: vars.clone(), + warp_account_addr: Some(job.account.to_string()), }), - gas_limit: None, - reply_on: ReplyOn::Always, - }); + ); + + match resolution { + Ok(true) => { + submsgs.push(SubMsg { + id: data.id.u64(), + msg: CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: job.account.to_string(), + msg: to_binary(&account::ExecuteMsg::WarpMsgs(WarpMsgs { + msgs: deps.querier.query_wasm_smart( + config.resolver_address, + &resolver::QueryMsg::QueryHydrateMsgs(QueryHydrateMsgsMsg { + msgs, + vars, + }), + )?, + job_id: Some(data.id), + }))?, + funds: vec![], + }), + gas_limit: None, + reply_on: ReplyOn::Always, + }); + break; + } + Ok(false) => { + // Continue to the next condition + continue; + } + Err(e) => { + attrs.push(Attribute::new("job_condition_status", "invalid")); + attrs.push(Attribute::new("error", e.to_string())); + JobQueue::finalize(deps.storage, env, job.id.into(), JobStatus::Failed)?; + break; + } + } } - let reward_msg = BankMsg::Send { - to_address: keeper_account.account.to_string(), - amount: vec![Coin::new(job.reward.u128(), config.fee_denom)], - }; + // Controller sends reward to executor + msgs.push(build_transfer_native_funds_msg( + info.sender.to_string(), + vec![Coin::new(job.reward.u128(), config.fee_denom)], + )); + + // Free account + msgs.push(build_free_job_account_msg( + config.account_tracker_address.to_string(), + job.owner.to_string(), + account_addr.to_string(), + job.id, + )); + + if let Some(funding_account) = job.funding_account { + msgs.push(build_free_funding_account_msg( + config.account_tracker_address.to_string(), + job.owner.to_string(), + funding_account.to_string(), + job.id, + )); + } Ok(Response::new() + .add_messages(msgs) .add_submessages(submsgs) - .add_message(reward_msg) .add_attribute("action", "execute_job") .add_attribute("executor", info.sender) .add_attribute("job_id", job.id) @@ -423,130 +562,68 @@ pub fn evict_job( env: Env, info: MessageInfo, data: EvictJobMsg, + config: Config, ) -> Result { - let config = CONFIG.load(deps.storage)?; - let state = STATE.load(deps.storage)?; - let job = PENDING_JOBS().load(deps.storage, data.id.u64())?; - let account = ACCOUNTS().load(deps.storage, job.owner.clone())?; - - let account_amount = deps - .querier - .query::(&QueryRequest::Bank(BankQuery::Balance { - address: account.account.to_string(), - denom: config.fee_denom.clone(), - }))? - .amount - .amount; + let job = JobQueue::get(deps.storage, data.id.into())?; + let account_addr = job.account.clone(); if job.status != JobStatus::Pending { return Err(ContractError::Unauthorized {}); } - let t = if state.q < config.q_max { - config.t_max - state.q * (config.t_max - config.t_min) / config.q_max - } else { - config.t_min - }; - - let a = if state.q < config.q_max { - config.a_min - } else { - config.a_max - }; + let eviction_fee = config.maintenance_fee_min; - if env.block.time.seconds() - job.last_update_time.u64() < t.u64() { - return Err(EvictionPeriodNotElapsed {}); + if (env.block.time.seconds() - job.created_at_time.u64()) < (job.duration_days.u64() * 86400) { + return Err(ContractError::EvictionPeriodNotElapsed {}); } - let mut cosmos_msgs = vec![]; - - let job_status; - - if job.requeue_on_evict && account_amount >= a { - cosmos_msgs.push( - //send reward to evictor - CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: account.account.to_string(), - msg: to_binary(&account::ExecuteMsg::Generic(GenericMsg { - msgs: vec![CosmosMsg::Bank(BankMsg::Send { - to_address: info.sender.to_string(), - amount: vec![Coin::new(a.u128(), config.fee_denom)], - })], - }))?, - funds: vec![], - }), - ); - job_status = PENDING_JOBS() - .update(deps.storage, data.id.u64(), |j| match j { - None => Err(ContractError::JobDoesNotExist {}), - Some(job) => Ok(Job { - id: job.id, - owner: job.owner, - last_update_time: Uint64::new(env.block.time.seconds()), - name: job.name, - description: job.description, - labels: job.labels, - status: JobStatus::Pending, - condition: job.condition, - terminate_condition: job.terminate_condition, - msgs: job.msgs, - vars: job.vars, - recurring: job.recurring, - requeue_on_evict: job.requeue_on_evict, - reward: job.reward, - assets_to_withdraw: job.assets_to_withdraw, - }), - })? - .status; - } else { - PENDING_JOBS().remove(deps.storage, data.id.u64())?; - job_status = FINISHED_JOBS() - .update(deps.storage, data.id.u64(), |j| match j { - None => Ok(Job { - id: job.id, - owner: job.owner, - last_update_time: Uint64::new(env.block.time.seconds()), - name: job.name, - description: job.description, - labels: job.labels, - status: JobStatus::Evicted, - condition: job.condition, - terminate_condition: job.terminate_condition, - msgs: job.msgs, - vars: job.vars, - recurring: job.recurring, - requeue_on_evict: job.requeue_on_evict, - reward: job.reward, - assets_to_withdraw: job.assets_to_withdraw, - }), - Some(_) => Err(ContractError::JobAlreadyExists {}), - })? - .status; - - cosmos_msgs.append(&mut vec![ - //send reward minus fee back to account - CosmosMsg::Bank(BankMsg::Send { - to_address: info.sender.to_string(), - amount: vec![Coin::new(a.u128(), config.fee_denom.clone())], - }), - CosmosMsg::Bank(BankMsg::Send { - to_address: account.account.to_string(), - amount: vec![Coin::new((job.reward - a).u128(), config.fee_denom)], - }), - ]); - - STATE.save( - deps.storage, - &State { - current_job_id: state.current_job_id, - q: state.q.checked_sub(Uint64::new(1))?, - }, - )?; + let mut msgs = vec![]; + + // Job will be evicted + let job_status = + JobQueue::finalize(deps.storage, env, job.id.into(), JobStatus::Evicted)?.status; + + // Controller sends eviction reward to evictor + msgs.push(build_transfer_native_funds_msg( + info.sender.to_string(), + vec![Coin::new(eviction_fee.u128(), config.fee_denom.clone())], + )); + + // Controller sends execution reward minus eviction reward back to owner + msgs.push(build_transfer_native_funds_msg( + job.owner.to_string(), + vec![Coin::new( + (job.reward - eviction_fee).u128(), + config.fee_denom.clone(), + )], + )); + + // Free account + msgs.push(build_free_job_account_msg( + config.account_tracker_address.to_string(), + job.owner.to_string(), + account_addr.to_string(), + job.id, + )); + + if let Some(funding_account) = job.funding_account { + msgs.push(build_free_funding_account_msg( + config.account_tracker_address.to_string(), + job.owner.to_string(), + funding_account.to_string(), + job.id, + )); } + // Job owner withdraw all assets that are listed from warp account to itself + msgs.push(build_account_withdraw_assets_msg( + account_addr.to_string(), + job.assets_to_withdraw, + )); + Ok(Response::new() + .add_messages(msgs) .add_attribute("action", "evict_job") .add_attribute("job_id", job.id) - .add_attribute("job_status", serde_json_wasm::to_string(&job_status)?) - .add_messages(cosmos_msgs)) + .add_attribute("job_status", serde_json_wasm::to_string(&job_status)?)) } diff --git a/contracts/warp-controller/src/execute/mod.rs b/contracts/warp-controller/src/execute/mod.rs index ee75d71c..e4a83744 100644 --- a/contracts/warp-controller/src/execute/mod.rs +++ b/contracts/warp-controller/src/execute/mod.rs @@ -1,3 +1,4 @@ pub(crate) mod account; pub(crate) mod controller; +pub(crate) mod fee; pub(crate) mod job; diff --git a/contracts/warp-controller/src/lib.rs b/contracts/warp-controller/src/lib.rs index b9b3cea3..80857688 100644 --- a/contracts/warp-controller/src/lib.rs +++ b/contracts/warp-controller/src/lib.rs @@ -5,7 +5,10 @@ pub mod state; pub use crate::error::ContractError; mod execute; +mod migrate; mod query; +mod reply; + #[cfg(test)] mod tests; mod util; diff --git a/contracts/warp-controller/src/migrate/account.rs b/contracts/warp-controller/src/migrate/account.rs new file mode 100644 index 00000000..e4ef9fac --- /dev/null +++ b/contracts/warp-controller/src/migrate/account.rs @@ -0,0 +1,37 @@ +use cosmwasm_std::{to_binary, Deps, Env, MessageInfo, Response, WasmMsg}; + +use crate::ContractError; +use account_tracker::{AccountsResponse, MigrateMsg, QueryAccountsMsg}; +use controller::{Config, MigrateAccountsMsg}; + +pub fn migrate_accounts( + deps: Deps, + _env: Env, + info: MessageInfo, + msg: MigrateAccountsMsg, + config: Config, +) -> Result { + if info.sender != config.owner { + return Err(ContractError::Unauthorized {}); + } + + let accounts: AccountsResponse = deps.querier.query_wasm_smart( + config.account_tracker_address, + &account_tracker::QueryMsg::QueryAccounts(QueryAccountsMsg { + account_owner_addr: msg.account_owner_addr, + start_after: msg.start_after, + limit: Some(msg.limit as u32), + }), + )?; + + let mut migration_msgs = vec![]; + for account in accounts.accounts { + migration_msgs.push(WasmMsg::Migrate { + contract_addr: account.account_addr.to_string(), + new_code_id: msg.warp_account_code_id.u64(), + msg: to_binary(&MigrateMsg {})?, + }); + } + + Ok(Response::new().add_messages(migration_msgs)) +} diff --git a/contracts/warp-controller/src/migrate/job.rs b/contracts/warp-controller/src/migrate/job.rs new file mode 100644 index 00000000..90e4175c --- /dev/null +++ b/contracts/warp-controller/src/migrate/job.rs @@ -0,0 +1,182 @@ +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{DepsMut, Env, MessageInfo, Response}; + +use crate::state::{FINISHED_JOBS, PENDING_JOBS}; +use crate::{state::CONFIG, ContractError}; + +use controller::account::AssetInfo; +use controller::job::{Execution, Job, JobStatus}; +use controller::MigrateJobsMsg; +use cosmwasm_std::{Addr, Order, Uint128, Uint64}; +use cw_storage_plus::{Bound, Index, IndexList, IndexedMap, MultiIndex, UniqueIndex}; + +#[cw_serde] +pub struct OldJob { + pub id: Uint64, + pub prev_id: Option, + pub owner: Addr, + pub account: Addr, + pub last_update_time: Uint64, + pub name: String, + pub description: String, + pub labels: Vec, + pub status: JobStatus, + pub terminate_condition: Option, + pub duration_days: Uint64, + pub executions: Vec, + pub vars: String, + pub recurring: bool, + pub requeue_on_evict: bool, + pub reward: Uint128, + pub assets_to_withdraw: Vec, +} + +pub struct OldJobIndexes<'a> { + pub reward: UniqueIndex<'a, (u128, u64), OldJob>, + pub publish_time: MultiIndex<'a, u64, OldJob, u64>, +} + +impl IndexList for OldJobIndexes<'_> { + fn get_indexes(&'_ self) -> Box> + '_> { + let v: Vec<&dyn Index> = vec![&self.reward, &self.publish_time]; + Box::new(v.into_iter()) + } +} + +pub fn migrate_pending_jobs( + deps: DepsMut, + _env: Env, + info: MessageInfo, + msg: MigrateJobsMsg, +) -> Result { + let config = CONFIG.load(deps.storage)?; + if info.sender != config.owner { + return Err(ContractError::Unauthorized {}); + } + + let start_after = msg.start_after; + let start_after = start_after.map(Bound::exclusive); + + #[allow(non_snake_case)] + pub fn OLD_PENDING_JOBS<'a>() -> IndexedMap<'a, u64, OldJob, OldJobIndexes<'a>> { + let indexes = OldJobIndexes { + reward: UniqueIndex::new( + |job| (job.reward.u128(), job.id.u64()), + "pending_jobs__reward_v5", + ), + publish_time: MultiIndex::new( + |_pk, job| job.last_update_time.u64(), + "pending_jobs_v5", + "pending_jobs__publish_timestamp_v5", + ), + }; + IndexedMap::new("pending_jobs_v5", indexes) + } + + let job_keys: Result, _> = OLD_PENDING_JOBS() + .keys(deps.storage, start_after, None, Order::Ascending) + .take(msg.limit as usize) + .collect(); + let job_keys = job_keys?; + + for job_key in job_keys { + let old_job = OLD_PENDING_JOBS().load(deps.storage, job_key)?; + + PENDING_JOBS().save( + deps.storage, + job_key, + &Job { + id: old_job.id, + prev_id: old_job.prev_id, + owner: old_job.owner, + account: old_job.account, + last_update_time: old_job.last_update_time, + name: old_job.name, + description: old_job.description, + labels: old_job.labels, + status: old_job.status, + terminate_condition: old_job.terminate_condition, + executions: old_job.executions, + vars: old_job.vars, + recurring: old_job.recurring, + reward: old_job.reward, + assets_to_withdraw: old_job.assets_to_withdraw, + duration_days: old_job.duration_days, + created_at_time: old_job.last_update_time, + // TODO: update to old_job.funding_account + funding_account: None, + }, + )?; + } + + Ok(Response::new()) +} + +pub fn migrate_finished_jobs( + deps: DepsMut, + _env: Env, + info: MessageInfo, + msg: MigrateJobsMsg, +) -> Result { + let config = CONFIG.load(deps.storage)?; + if info.sender != config.owner { + return Err(ContractError::Unauthorized {}); + } + + let start_after = msg.start_after; + let start_after = start_after.map(Bound::exclusive); + + #[allow(non_snake_case)] + pub fn OLD_FINISHED_JOBS<'a>() -> IndexedMap<'a, u64, OldJob, OldJobIndexes<'a>> { + let indexes = OldJobIndexes { + reward: UniqueIndex::new( + |job| (job.reward.u128(), job.id.u64()), + "finished_jobs__reward_v5", + ), + publish_time: MultiIndex::new( + |_pk, job| job.last_update_time.u64(), + "finished_jobs_v5", + "finished_jobs__publish_timestamp_v5", + ), + }; + IndexedMap::new("finished_jobs_v5", indexes) + } + + let job_keys: Result, _> = OLD_FINISHED_JOBS() + .keys(deps.storage, start_after, None, Order::Ascending) + .take(msg.limit as usize) + .collect(); + let job_keys = job_keys?; + + for job_key in job_keys { + let old_job = OLD_FINISHED_JOBS().load(deps.storage, job_key)?; + + FINISHED_JOBS().save( + deps.storage, + job_key, + &Job { + id: old_job.id, + prev_id: old_job.prev_id, + owner: old_job.owner, + account: old_job.account, + last_update_time: old_job.last_update_time, + name: old_job.name, + description: old_job.description, + labels: old_job.labels, + status: old_job.status, + executions: old_job.executions, + terminate_condition: old_job.terminate_condition, + vars: old_job.vars, + recurring: old_job.recurring, + reward: old_job.reward, + assets_to_withdraw: old_job.assets_to_withdraw, + duration_days: old_job.duration_days, + created_at_time: old_job.last_update_time, + // TODO: update to old_job.funding_account + funding_account: None, + }, + )?; + } + + Ok(Response::new()) +} diff --git a/contracts/warp-controller/src/migrate/mod.rs b/contracts/warp-controller/src/migrate/mod.rs new file mode 100644 index 00000000..d882e236 --- /dev/null +++ b/contracts/warp-controller/src/migrate/mod.rs @@ -0,0 +1,2 @@ +pub(crate) mod account; +pub(crate) mod job; diff --git a/contracts/warp-controller/src/query/account.rs b/contracts/warp-controller/src/query/account.rs deleted file mode 100644 index 2e953dcf..00000000 --- a/contracts/warp-controller/src/query/account.rs +++ /dev/null @@ -1,31 +0,0 @@ -use crate::state::{ACCOUNTS, QUERY_PAGE_SIZE}; -use controller::account::{AccountResponse, AccountsResponse, QueryAccountMsg, QueryAccountsMsg}; -use cosmwasm_std::{Deps, Env, Order, StdResult}; -use cw_storage_plus::Bound; - -pub fn query_account(deps: Deps, _env: Env, data: QueryAccountMsg) -> StdResult { - Ok(AccountResponse { - account: ACCOUNTS().load(deps.storage, deps.api.addr_validate(data.owner.as_str())?)?, - }) -} - -pub fn query_accounts( - deps: Deps, - _env: Env, - data: QueryAccountsMsg, -) -> StdResult { - let start_after = match data.start_after { - None => None, - Some(s) => Some(deps.api.addr_validate(s.as_str())?), - }; - let start_after = start_after.map(Bound::exclusive); - let infos = ACCOUNTS() - .range(deps.storage, start_after, None, Order::Ascending) - .take(data.limit.unwrap_or(QUERY_PAGE_SIZE) as usize) - .collect::>>()?; - let mut accounts = vec![]; - for tuple in infos { - accounts.push(tuple.1) - } - Ok(AccountsResponse { accounts }) -} diff --git a/contracts/warp-controller/src/query/controller.rs b/contracts/warp-controller/src/query/controller.rs index 6a006842..630c5118 100644 --- a/contracts/warp-controller/src/query/controller.rs +++ b/contracts/warp-controller/src/query/controller.rs @@ -1,8 +1,13 @@ -use crate::state::CONFIG; -use controller::{ConfigResponse, QueryConfigMsg}; +use crate::state::{CONFIG, STATE}; +use controller::{ConfigResponse, QueryConfigMsg, QueryStateMsg, StateResponse}; use cosmwasm_std::{Deps, Env, StdResult}; pub fn query_config(deps: Deps, _env: Env, _data: QueryConfigMsg) -> StdResult { let config = CONFIG.load(deps.storage)?; Ok(ConfigResponse { config }) } + +pub fn query_state(deps: Deps, _env: Env, _data: QueryStateMsg) -> StdResult { + let state = STATE.load(deps.storage)?; + Ok(StateResponse { state }) +} diff --git a/contracts/warp-controller/src/query/job.rs b/contracts/warp-controller/src/query/job.rs index 4af58e70..f8e14dc4 100644 --- a/contracts/warp-controller/src/query/job.rs +++ b/contracts/warp-controller/src/query/job.rs @@ -67,7 +67,7 @@ pub fn query_jobs(deps: Deps, env: Env, data: QueryJobsMsg) -> StdResult, job_status: Option, start_after: Option<(u128, u64)>, - limit: usize, + limit: u32, ) -> StdResult { let start = start_after.map(Bound::exclusive); let map = if job_status.is_some() && job_status.clone().unwrap() != JobStatus::Pending { @@ -135,7 +135,7 @@ pub fn query_jobs_by_reward( job_status.clone(), ) }) - .take(limit) + .take(limit as usize) .collect::>>()?; let mut jobs = vec![]; @@ -144,7 +144,7 @@ pub fn query_jobs_by_reward( } Ok(JobsResponse { jobs, - total_count: infos.len(), + total_count: infos.len() as u32, }) } @@ -187,6 +187,6 @@ pub fn query_jobs_by_owner( } Ok(JobsResponse { jobs, - total_count: infos.len(), + total_count: infos.len() as u32, }) } diff --git a/contracts/warp-controller/src/query/mod.rs b/contracts/warp-controller/src/query/mod.rs index ee75d71c..b10bee4a 100644 --- a/contracts/warp-controller/src/query/mod.rs +++ b/contracts/warp-controller/src/query/mod.rs @@ -1,3 +1,2 @@ -pub(crate) mod account; pub(crate) mod controller; pub(crate) mod job; diff --git a/contracts/warp-controller/src/reply/account.rs b/contracts/warp-controller/src/reply/account.rs new file mode 100644 index 00000000..5cfedb1e --- /dev/null +++ b/contracts/warp-controller/src/reply/account.rs @@ -0,0 +1,190 @@ +use cosmwasm_std::{Coin, CosmosMsg, DepsMut, Env, Reply, Response, StdError, Uint64}; + +use controller::{account::CwFund, Config}; + +use crate::{ + state::JobQueue, + util::msg::{ + build_free_funding_account_msg, build_take_job_account_msg, build_transfer_cw20_msg, + build_transfer_cw721_msg, + }, + ContractError, +}; + +pub fn create_account_and_job( + deps: DepsMut, + env: Env, + msg: Reply, + config: Config, +) -> Result { + let reply = msg.result.into_result().map_err(StdError::generic_err)?; + + let account_event = reply + .events + .iter() + .find(|event| { + event + .attributes + .iter() + .any(|attr| attr.key == "action" && attr.value == "instantiate") + }) + .ok_or_else(|| StdError::generic_err("cannot find `instantiate` event"))?; + + let job_id_str = account_event + .attributes + .iter() + .cloned() + .find(|attr| attr.key == "job_id") + .ok_or_else(|| StdError::generic_err("cannot find `job_id` attribute"))? + .value; + let job_id = job_id_str.as_str().parse::()?; + + let owner = account_event + .attributes + .iter() + .cloned() + .find(|attr| attr.key == "owner") + .ok_or_else(|| StdError::generic_err("cannot find `owner` attribute"))? + .value; + + let account_addr = deps.api.addr_validate( + &account_event + .attributes + .iter() + .cloned() + .find(|attr| attr.key == "contract_addr") + .ok_or_else(|| StdError::generic_err("cannot find `contract_addr` attribute"))? + .value, + )?; + + let native_funds: Vec = serde_json_wasm::from_str( + &account_event + .attributes + .iter() + .cloned() + .find(|attr| attr.key == "native_funds") + .ok_or_else(|| StdError::generic_err("cannot find `native_funds` attribute"))? + .value, + )?; + + let cw_funds: Option> = serde_json_wasm::from_str( + &account_event + .attributes + .iter() + .cloned() + .find(|attr| attr.key == "cw_funds") + .ok_or_else(|| StdError::generic_err("cannot find `cw_funds` attribute"))? + .value, + )?; + + let mut job = JobQueue::get(deps.storage, job_id)?; + job.account = account_addr.clone(); + JobQueue::sync(deps.storage, env, job.clone())?; + + let mut msgs: Vec = vec![]; + + if let Some(cw_funds) = cw_funds.clone() { + // Fund account in CW20 / CW721 tokens + for cw_fund in cw_funds { + msgs.push(match cw_fund { + CwFund::Cw20(cw20_fund) => build_transfer_cw20_msg( + deps.api + .addr_validate(&cw20_fund.contract_addr)? + .to_string(), + owner.clone(), + account_addr.clone().to_string(), + cw20_fund.amount, + ), + CwFund::Cw721(cw721_fund) => build_transfer_cw721_msg( + deps.api + .addr_validate(&cw721_fund.contract_addr)? + .to_string(), + account_addr.clone().to_string(), + cw721_fund.token_id.clone(), + ), + }) + } + } + + // Take job account + msgs.push(build_take_job_account_msg( + config.account_tracker_address.to_string(), + job.owner.to_string(), + account_addr.to_string(), + job.id, + )); + + Ok(Response::new() + .add_messages(msgs) + .add_attribute("action", "create_account_and_job_reply") + .add_attribute("job_id", job_id.to_string()) + .add_attribute("owner", owner) + .add_attribute("account_address", account_addr) + .add_attribute("native_funds", serde_json_wasm::to_string(&native_funds)?) + .add_attribute( + "cw_funds", + serde_json_wasm::to_string(&cw_funds.unwrap_or(vec![]))?, + )) +} + +pub fn create_funding_account( + deps: DepsMut, + _env: Env, + msg: Reply, + config: Config, +) -> Result { + let reply = msg.result.into_result().map_err(StdError::generic_err)?; + + let funding_account_event = reply + .events + .iter() + .find(|event| { + event + .attributes + .iter() + .any(|attr| attr.key == "action" && attr.value == "instantiate") + }) + .ok_or_else(|| StdError::generic_err("cannot find `instantiate` event"))?; + + let owner = funding_account_event + .attributes + .iter() + .cloned() + .find(|attr| attr.key == "owner") + .ok_or_else(|| StdError::generic_err("cannot find `owner` attribute"))? + .value; + + let funding_account_addr = deps.api.addr_validate( + &funding_account_event + .attributes + .iter() + .cloned() + .find(|attr| attr.key == "contract_addr") + .ok_or_else(|| StdError::generic_err("cannot find `contract_addr` attribute"))? + .value, + )?; + + let native_funds: Vec = serde_json_wasm::from_str( + &funding_account_event + .attributes + .iter() + .cloned() + .find(|attr| attr.key == "native_funds") + .ok_or_else(|| StdError::generic_err("cannot find `native_funds` attribute"))? + .value, + )?; + + let msgs: Vec = vec![build_free_funding_account_msg( + config.account_tracker_address.to_string(), + owner.to_string(), + funding_account_addr.to_string(), + Uint64::from(0u64), // placeholder, + )]; + + Ok(Response::new() + .add_messages(msgs) + .add_attribute("action", "create_funding_account_reply") + .add_attribute("owner", owner) + .add_attribute("funding_account_address", funding_account_addr) + .add_attribute("native_funds", serde_json_wasm::to_string(&native_funds)?)) +} diff --git a/contracts/warp-controller/src/reply/job.rs b/contracts/warp-controller/src/reply/job.rs new file mode 100644 index 00000000..ee4880eb --- /dev/null +++ b/contracts/warp-controller/src/reply/job.rs @@ -0,0 +1,286 @@ +use cosmwasm_std::{ + Attribute, BalanceResponse, BankQuery, Coin, DepsMut, Env, QueryRequest, Reply, Response, + StdError, StdResult, SubMsgResult, Uint64, +}; + +use crate::{ + error::map_contract_error, + execute::fee::{compute_burn_fee, compute_creation_fee, compute_maintenance_fee}, + state::{JobQueue, CONFIG, STATE}, + util::msg::{ + build_account_execute_generic_msgs, build_account_withdraw_assets_msg, + build_take_funding_account_msg, build_take_job_account_msg, + build_transfer_native_funds_msg, + }, + ContractError, +}; +use controller::{ + job::{Job, JobStatus}, + Config, +}; + +pub fn execute_job( + deps: DepsMut, + env: Env, + msg: Reply, + config: Config, +) -> Result { + let state = STATE.load(deps.storage)?; + + let new_status = match msg.result { + SubMsgResult::Ok(_) => JobStatus::Executed, + SubMsgResult::Err(_) => JobStatus::Failed, + }; + + let job_id = msg.id; + + let finished_job = JobQueue::finalize(deps.storage, env.clone(), job_id, new_status)?; + + let res_attrs = match msg.result { + SubMsgResult::Err(e) => vec![Attribute::new( + "transaction_error", + format!("{}. {}", &e, map_contract_error(&e)), + )], + _ => vec![], + }; + + let mut msgs = vec![]; + let mut new_job_attrs = vec![]; + let new_job_id = state.current_job_id; + + let creation_fee = compute_creation_fee(state.q, &config); + let maintenance_fee = compute_maintenance_fee(finished_job.duration_days, &config); + let burn_fee = compute_burn_fee(finished_job.reward, &config); + + let total_fees = creation_fee + maintenance_fee + burn_fee; + + let reward_plus_fee = finished_job.reward + total_fees; + + let account_addr = finished_job.account.clone(); + + let mut recurring_job_created = false; + + if finished_job.recurring { + let funding_account_addr = finished_job.funding_account.clone().unwrap(); + + let operational_amount = deps + .querier + .query::(&QueryRequest::Bank(BankQuery::Balance { + address: funding_account_addr.to_string(), + denom: config.fee_denom.clone(), + }))? + .amount + .amount; + + if operational_amount < reward_plus_fee { + new_job_attrs.push(Attribute::new("action", "recur_job")); + new_job_attrs.push(Attribute::new("creation_status", "failed_insufficient_fee")); + } else if !(finished_job.status == JobStatus::Executed + || finished_job.status == JobStatus::Failed) + { + new_job_attrs.push(Attribute::new("action", "recur_job")); + new_job_attrs.push(Attribute::new( + "creation_status", + "failed_invalid_job_status", + )); + } else { + // vars are updated to next job iteration + let new_vars: String = deps.querier.query_wasm_smart( + config.resolver_address.clone(), + &resolver::QueryMsg::QueryApplyVarFn(resolver::QueryApplyVarFnMsg { + vars: finished_job.vars, + status: finished_job.status.clone(), + warp_account_addr: Some(finished_job.account.to_string()), + }), + )?; + + let should_terminate_job: bool; + + // check if terminate condition is true with updated vars + match finished_job.terminate_condition.clone() { + Some(terminate_condition) => { + let resolution: StdResult = deps.querier.query_wasm_smart( + config.resolver_address, + &resolver::QueryMsg::QueryResolveCondition( + resolver::QueryResolveConditionMsg { + condition: terminate_condition, + vars: new_vars.clone(), + warp_account_addr: Some(finished_job.account.to_string()), + }, + ), + ); + if let Err(e) = resolution { + should_terminate_job = true; + new_job_attrs.push(Attribute::new("action", "recur_job")); + new_job_attrs + .push(Attribute::new("job_terminate_condition_status", "invalid")); + new_job_attrs.push(Attribute::new( + "creation_status", + format!( + "terminated_due_to_terminate_condition_resolves_to_error. {}", + e + ), + )); + } else { + new_job_attrs + .push(Attribute::new("job_terminate_condition_status", "valid")); + if resolution? { + should_terminate_job = true; + new_job_attrs.push(Attribute::new("action", "recur_job")); + new_job_attrs.push(Attribute::new( + "creation_status", + "terminated_due_to_terminate_condition_resolves_to_true", + )); + } else { + should_terminate_job = false; + } + } + } + None => { + should_terminate_job = false; + } + } + + if !should_terminate_job { + recurring_job_created = true; + + let new_job = JobQueue::add( + deps.storage, + Job { + id: new_job_id, + prev_id: Some(finished_job.id), + owner: finished_job.owner.clone(), + account: finished_job.account.clone(), + last_update_time: Uint64::from(env.block.time.seconds()), + name: finished_job.name.clone(), + description: finished_job.description, + labels: finished_job.labels, + status: JobStatus::Pending, + executions: finished_job.executions, + terminate_condition: finished_job.terminate_condition.clone(), + vars: new_vars, + recurring: finished_job.recurring, + reward: finished_job.reward, + assets_to_withdraw: finished_job.assets_to_withdraw.clone(), + duration_days: finished_job.duration_days, + created_at_time: Uint64::from(env.block.time.seconds()), + funding_account: finished_job.funding_account.clone(), + }, + )?; + + msgs.push(build_account_execute_generic_msgs( + funding_account_addr.to_string(), + vec![ + // Job owner's funding account sends fee to fee collector + build_transfer_native_funds_msg( + config.fee_collector.to_string(), + vec![Coin::new(total_fees.u128(), config.fee_denom.clone())], + ), + // Job owner's funding account sends reward to controller + build_transfer_native_funds_msg( + env.contract.address.to_string(), + vec![Coin::new(new_job.reward.u128(), config.fee_denom.clone())], + ), + ], + )); + + new_job_attrs.push(Attribute::new("action", "create_job")); + new_job_attrs.push(Attribute::new("job_id", new_job.id)); + new_job_attrs.push(Attribute::new("job_owner", new_job.owner)); + new_job_attrs.push(Attribute::new("job_name", new_job.name)); + new_job_attrs.push(Attribute::new( + "job_status", + serde_json_wasm::to_string(&new_job.status)?, + )); + new_job_attrs.push(Attribute::new( + "job_executions", + serde_json_wasm::to_string(&new_job.executions)?, + )); + new_job_attrs.push(Attribute::new("job_reward", new_job.reward)); + new_job_attrs.push(Attribute::new("job_creation_fee", creation_fee.to_string())); + new_job_attrs.push(Attribute::new( + "job_maintenance_fee", + maintenance_fee.to_string(), + )); + new_job_attrs.push(Attribute::new("job_burn_fee", burn_fee.to_string())); + new_job_attrs.push(Attribute::new("job_total_fees", total_fees.to_string())); + new_job_attrs.push(Attribute::new( + "job_last_updated_time", + new_job.last_update_time, + )); + new_job_attrs.push(Attribute::new("sub_action", "recur_job")); + } + } + } + + if recurring_job_created { + let funding_account_addr = finished_job.funding_account.clone().unwrap(); + + // Take job account with the new job, previously freed in execute_job + msgs.push(build_take_job_account_msg( + config.account_tracker_address.to_string(), + finished_job.owner.to_string(), + account_addr.to_string(), + new_job_id, + )); + + // take funding account with new job, previously freed in execute_job + msgs.push(build_take_funding_account_msg( + config.account_tracker_address.to_string(), + finished_job.owner.to_string(), + funding_account_addr.to_string(), + new_job_id, + )); + } else { + // No new job created, account has been free in execute_job, no need to free here again + // Job owner withdraw all assets that are listed from warp account to itself + msgs.push(build_account_withdraw_assets_msg( + account_addr.to_string(), + finished_job.assets_to_withdraw, + )); + } + + Ok(Response::new() + .add_messages(msgs) + .add_attribute("action", "execute_job_reply") + .add_attribute("job_id", finished_job.id) + .add_attributes(res_attrs) + .add_attributes(new_job_attrs)) +} + +pub fn instantiate_sub_contracts( + deps: DepsMut, + _env: Env, + msg: Reply, + mut config: Config, +) -> Result { + let reply: cosmwasm_std::SubMsgResponse = + msg.result.into_result().map_err(StdError::generic_err)?; + + let account_tracker_instantiate_event = reply + .events + .iter() + .find(|event| { + event + .attributes + .iter() + .any(|attr| attr.key == "action" && attr.value == "instantiate") + }) + .ok_or_else(|| StdError::generic_err("cannot find `instantiate` event"))?; + + let account_tracker_addr = deps.api.addr_validate( + &account_tracker_instantiate_event + .attributes + .iter() + .cloned() + .find(|attr| attr.key == "account_tracker") + .ok_or_else(|| StdError::generic_err("cannot find `account_tracker` attribute"))? + .value, + )?; + + config.account_tracker_address = account_tracker_addr; + + CONFIG.save(deps.storage, &config)?; + + Ok(Response::new().add_attribute("action", "instantiate_sub_contracts_reply")) +} diff --git a/contracts/warp-controller/src/reply/mod.rs b/contracts/warp-controller/src/reply/mod.rs new file mode 100644 index 00000000..d882e236 --- /dev/null +++ b/contracts/warp-controller/src/reply/mod.rs @@ -0,0 +1,2 @@ +pub(crate) mod account; +pub(crate) mod job; diff --git a/contracts/warp-controller/src/state.rs b/contracts/warp-controller/src/state.rs index dc245a56..8ba9018d 100644 --- a/contracts/warp-controller/src/state.rs +++ b/contracts/warp-controller/src/state.rs @@ -1,9 +1,12 @@ -use controller::account::Account; -use cosmwasm_std::Addr; +use cosmwasm_std::{Env, Storage, Uint64}; use cw_storage_plus::{Index, IndexList, IndexedMap, Item, MultiIndex, UniqueIndex}; -use controller::job::Job; -use controller::{Config, State}; +use controller::{ + job::{Job, JobStatus, UpdateJobMsg}, + Config, State, +}; + +use crate::ContractError; pub struct JobIndexes<'a> { pub reward: UniqueIndex<'a, (u128, u64), Job>, @@ -23,20 +26,20 @@ pub fn PENDING_JOBS<'a>() -> IndexedMap<'a, u64, Job, JobIndexes<'a>> { let indexes = JobIndexes { reward: UniqueIndex::new( |job| (job.reward.u128(), job.id.u64()), - "pending_jobs__reward_v3", + "pending_jobs__reward_v6", ), publish_time: MultiIndex::new( |_pk, job| job.last_update_time.u64(), - "pending_jobs_v3", - "pending_jobs__publish_timestamp_v3", + "pending_jobs_v6", + "pending_jobs__publish_timestamp_v6", ), owner: MultiIndex::new( |_pk, job| job.owner.to_string(), - "pending_jobs_v3", - "pending_jobs__owner_v3", + "pending_jobs_v6", + "pending_jobs__owner_v6", ), }; - IndexedMap::new("pending_jobs_v3", indexes) + IndexedMap::new("pending_jobs_v6", indexes) } #[allow(non_snake_case)] @@ -44,41 +47,163 @@ pub fn FINISHED_JOBS<'a>() -> IndexedMap<'a, u64, Job, JobIndexes<'a>> { let indexes = JobIndexes { reward: UniqueIndex::new( |job| (job.reward.u128(), job.id.u64()), - "finished_jobs__reward_v3", + "finished_jobs__reward_v6", ), publish_time: MultiIndex::new( |_pk, job| job.last_update_time.u64(), - "finished_jobs_v3", - "finished_jobs__publish_timestamp_v3", + "finished_jobs_v6", + "finished_jobs__publish_timestamp_v6", ), owner: MultiIndex::new( |_pk, job| job.owner.to_string(), - "finished_jobs_v3", - "finished_jobs__owner_v3", + "finished_jobs_v6", + "finished_jobs__owner_v6", ), }; - IndexedMap::new("finished_jobs_v3", indexes) + IndexedMap::new("finished_jobs_v6", indexes) } -pub struct AccountIndexes<'a> { - pub account: UniqueIndex<'a, Addr, Account>, -} +pub const QUERY_PAGE_SIZE: u32 = 50; +pub const CONFIG: Item = Item::new("config"); +pub const STATE: Item = Item::new("state"); -impl IndexList for AccountIndexes<'_> { - fn get_indexes(&'_ self) -> Box> + '_> { - let v: Vec<&dyn Index> = vec![&self.account]; - Box::new(v.into_iter()) +pub struct JobQueue; + +impl JobQueue { + pub fn add(storage: &mut dyn Storage, job: Job) -> Result { + let state: State = STATE.load(storage)?; + + let job = PENDING_JOBS().update(storage, state.current_job_id.u64(), |s| match s { + None => Ok(job), + Some(_) => Err(ContractError::JobAlreadyExists {}), + })?; + + STATE.save( + storage, + &State { + current_job_id: state.current_job_id.checked_add(Uint64::new(1))?, + q: state.q.checked_add(Uint64::new(1))?, + }, + )?; + + Ok(job) } -} -#[allow(non_snake_case)] -pub fn ACCOUNTS<'a>() -> IndexedMap<'a, Addr, Account, AccountIndexes<'a>> { - let indexes = AccountIndexes { - account: UniqueIndex::new(|account| account.account.clone(), "accounts__account"), - }; - IndexedMap::new("accounts", indexes) -} + pub fn get(storage: &dyn Storage, job_id: u64) -> Result { + let job = PENDING_JOBS().load(storage, job_id)?; -pub const QUERY_PAGE_SIZE: u32 = 50; -pub const CONFIG: Item = Item::new("config"); -pub const STATE: Item = Item::new("state"); + Ok(job) + } + + pub fn sync(storage: &mut dyn Storage, env: Env, job: Job) -> Result { + let res = PENDING_JOBS().update(storage, job.id.u64(), |j| match j { + None => Err(ContractError::JobDoesNotExist {}), + Some(_) => Ok(Job { + id: job.id, + prev_id: job.prev_id, + owner: job.owner, + account: job.account, + last_update_time: Uint64::new(env.block.time.seconds()), + name: job.name, + description: job.description, + labels: job.labels, + status: JobStatus::Pending, + executions: job.executions, + terminate_condition: job.terminate_condition, + vars: job.vars, + recurring: job.recurring, + reward: job.reward, + assets_to_withdraw: job.assets_to_withdraw, + duration_days: job.duration_days, + created_at_time: Uint64::from(env.block.time.seconds()), + funding_account: job.funding_account, + }), + })?; + + Ok(res) + } + + pub fn update( + storage: &mut dyn Storage, + env: Env, + data: UpdateJobMsg, + ) -> Result { + let job = PENDING_JOBS().update(storage, data.id.u64(), |h| match h { + None => Err(ContractError::JobDoesNotExist {}), + Some(job) => Ok(Job { + id: job.id, + prev_id: job.prev_id, + owner: job.owner, + account: job.account, + last_update_time: Uint64::new(env.block.time.seconds()), + name: data.name.unwrap_or(job.name), + description: data.description.unwrap_or(job.description), + labels: data.labels.unwrap_or(job.labels), + status: job.status, + executions: job.executions, + terminate_condition: job.terminate_condition, + vars: job.vars, + recurring: job.recurring, + reward: job.reward, + assets_to_withdraw: job.assets_to_withdraw, + duration_days: job.duration_days, + created_at_time: job.created_at_time, + funding_account: job.funding_account, + }), + })?; + + Ok(job) + } + + pub fn finalize( + storage: &mut dyn Storage, + env: Env, + job_id: u64, + status: JobStatus, + ) -> Result { + if status == JobStatus::Pending { + return Err(ContractError::Unauthorized {}); + } + + let job = PENDING_JOBS().load(storage, job_id)?; + + let new_job = Job { + id: job.id, + prev_id: job.prev_id, + owner: job.owner, + account: job.account, + last_update_time: Uint64::new(env.block.time.seconds()), + name: job.name, + description: job.description, + labels: job.labels, + status, + terminate_condition: job.terminate_condition, + executions: job.executions, + vars: job.vars, + recurring: job.recurring, + reward: job.reward, + assets_to_withdraw: job.assets_to_withdraw, + duration_days: job.duration_days, + created_at_time: job.created_at_time, + funding_account: job.funding_account, + }; + + FINISHED_JOBS().update(storage, job_id, |j| match j { + None => Ok(new_job.clone()), + Some(_) => Err(ContractError::JobAlreadyFinished {}), + })?; + + PENDING_JOBS().remove(storage, job_id)?; + + let state = STATE.load(storage)?; + STATE.save( + storage, + &State { + current_job_id: state.current_job_id, + q: state.q.checked_sub(Uint64::new(1))?, + }, + )?; + + Ok(new_job) + } +} diff --git a/contracts/warp-controller/src/tests/execute/account/mod.rs b/contracts/warp-controller/src/tests/execute/account/mod.rs deleted file mode 100644 index 239941ca..00000000 --- a/contracts/warp-controller/src/tests/execute/account/mod.rs +++ /dev/null @@ -1 +0,0 @@ -mod test_create_account; diff --git a/contracts/warp-controller/src/tests/execute/account/test_create_account.rs b/contracts/warp-controller/src/tests/execute/account/test_create_account.rs deleted file mode 100644 index 8b137891..00000000 --- a/contracts/warp-controller/src/tests/execute/account/test_create_account.rs +++ /dev/null @@ -1 +0,0 @@ - diff --git a/contracts/warp-controller/src/tests/execute/mod.rs b/contracts/warp-controller/src/tests/execute/mod.rs index 4abba991..be5bbc75 100644 --- a/contracts/warp-controller/src/tests/execute/mod.rs +++ b/contracts/warp-controller/src/tests/execute/mod.rs @@ -1,4 +1,3 @@ -mod account; mod controller; mod job; mod template; diff --git a/contracts/warp-controller/src/util/fee.rs b/contracts/warp-controller/src/util/fee.rs new file mode 100644 index 00000000..fdfdb9f5 --- /dev/null +++ b/contracts/warp-controller/src/util/fee.rs @@ -0,0 +1,21 @@ +use cosmwasm_std::{Coin, Uint128}; + +pub fn deduct_from_native_funds( + funds: Vec, + fee_denom: String, + deducted: Uint128, +) -> Vec { + let mut funds = funds; + let mut deducted_amount = deducted; + for fund in funds.iter_mut() { + if fund.denom == fee_denom { + fund.amount = fund.amount.checked_sub(deducted_amount).unwrap(); + deducted_amount = Uint128::zero(); + } + } + + // Filter out coins with an amount of zero + funds.retain(|coin| !coin.amount.is_zero()); + + funds +} diff --git a/contracts/warp-controller/src/util/mod.rs b/contracts/warp-controller/src/util/mod.rs index ff67a763..e87a1010 100644 --- a/contracts/warp-controller/src/util/mod.rs +++ b/contracts/warp-controller/src/util/mod.rs @@ -1 +1,3 @@ +pub(crate) mod fee; pub(crate) mod filter; +pub(crate) mod msg; diff --git a/contracts/warp-controller/src/util/msg.rs b/contracts/warp-controller/src/util/msg.rs new file mode 100644 index 00000000..facbe1ed --- /dev/null +++ b/contracts/warp-controller/src/util/msg.rs @@ -0,0 +1,218 @@ +use cosmwasm_std::{to_binary, BankMsg, Coin, CosmosMsg, Uint128, Uint64, WasmMsg}; + +use account_tracker::{ + FreeFundingAccountMsg, FreeJobAccountMsg, TakeFundingAccountMsg, TakeJobAccountMsg, +}; +use controller::account::{ + AssetInfo, CwFund, FundTransferMsgs, TransferFromMsg, TransferNftMsg, WarpMsg, WarpMsgs, + WithdrawAssetsMsg, +}; + +#[allow(clippy::too_many_arguments)] +pub fn build_instantiate_account_tracker_msg( + admin_addr: String, + controller_addr: String, + code_id: u64, +) -> CosmosMsg { + CosmosMsg::Wasm(WasmMsg::Instantiate { + admin: Some(admin_addr.clone()), + code_id, + msg: to_binary(&account_tracker::InstantiateMsg { + admin: admin_addr, + warp_addr: controller_addr, + }) + .unwrap(), + funds: vec![], + label: "warp job account tracker".to_string(), + }) +} + +#[allow(clippy::too_many_arguments)] +pub fn build_instantiate_warp_account_msg( + job_id: Uint64, + admin_addr: String, + code_id: u64, + account_owner: String, + native_funds: Vec, + cw_funds: Option>, + msgs: Option>, +) -> CosmosMsg { + CosmosMsg::Wasm(WasmMsg::Instantiate { + admin: Some(admin_addr), + code_id, + msg: to_binary(&account::InstantiateMsg { + owner: account_owner.clone(), + job_id, + native_funds: native_funds.clone(), + cw_funds: cw_funds.unwrap_or(vec![]), + msgs: msgs.unwrap_or(vec![]), + }) + .unwrap(), + funds: native_funds, + label: format!("warp account, owner: {}", account_owner,), + }) +} + +pub fn build_free_job_account_msg( + account_tracker_addr: String, + account_owner_addr: String, + account_addr: String, + last_job_id: Uint64, +) -> CosmosMsg { + CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: account_tracker_addr, + msg: to_binary(&account_tracker::ExecuteMsg::FreeJobAccount( + FreeJobAccountMsg { + account_owner_addr, + account_addr, + last_job_id, + }, + )) + .unwrap(), + funds: vec![], + }) +} + +pub fn build_take_job_account_msg( + account_tracker_addr: String, + account_owner_addr: String, + account_addr: String, + job_id: Uint64, +) -> CosmosMsg { + CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: account_tracker_addr, + msg: to_binary(&account_tracker::ExecuteMsg::TakeJobAccount( + TakeJobAccountMsg { + account_owner_addr, + account_addr, + job_id, + }, + )) + .unwrap(), + funds: vec![], + }) +} + +pub fn build_free_funding_account_msg( + account_tracker_addr: String, + account_owner_addr: String, + account_addr: String, + job_id: Uint64, +) -> CosmosMsg { + CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: account_tracker_addr, + msg: to_binary(&account_tracker::ExecuteMsg::FreeFundingAccount( + FreeFundingAccountMsg { + account_owner_addr, + account_addr, + job_id, + }, + )) + .unwrap(), + funds: vec![], + }) +} + +pub fn build_take_funding_account_msg( + account_tracker_addr: String, + account_owner_addr: String, + account_addr: String, + job_id: Uint64, +) -> CosmosMsg { + CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: account_tracker_addr, + msg: to_binary(&account_tracker::ExecuteMsg::TakeFundingAccount( + TakeFundingAccountMsg { + account_owner_addr, + account_addr, + job_id, + }, + )) + .unwrap(), + funds: vec![], + }) +} + +pub fn build_transfer_cw20_msg( + cw20_token_contract_addr: String, + owner_addr: String, + recipient_addr: String, + amount: Uint128, +) -> CosmosMsg { + CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: cw20_token_contract_addr, + msg: to_binary(&FundTransferMsgs::TransferFrom(TransferFromMsg { + owner: owner_addr, + recipient: recipient_addr, + amount, + })) + .unwrap(), + funds: vec![], + }) +} + +pub fn build_transfer_cw721_msg( + cw721_token_contract_addr: String, + recipient_addr: String, + token_id: String, +) -> CosmosMsg { + CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: cw721_token_contract_addr, + msg: to_binary(&FundTransferMsgs::TransferNft(TransferNftMsg { + recipient: recipient_addr, + token_id, + })) + .unwrap(), + funds: vec![], + }) +} + +pub fn build_transfer_native_funds_msg( + recipient_addr: String, + native_funds: Vec, +) -> CosmosMsg { + CosmosMsg::Bank(BankMsg::Send { + to_address: recipient_addr, + amount: native_funds, + }) +} + +pub fn build_account_execute_generic_msgs( + account_addr: String, + cosmos_msgs_for_account_to_execute: Vec, +) -> CosmosMsg { + build_account_execute_warp_msgs( + account_addr, + cosmos_msgs_for_account_to_execute + .into_iter() + .map(WarpMsg::Generic) + .collect(), + ) +} + +pub fn build_account_execute_warp_msgs( + account_addr: String, + warp_msgs_for_account_to_execute: Vec, +) -> CosmosMsg { + CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: account_addr, + msg: to_binary(&account::ExecuteMsg::WarpMsgs(WarpMsgs { + msgs: warp_msgs_for_account_to_execute, + job_id: None, + })) + .unwrap(), + funds: vec![], + }) +} + +pub fn build_account_withdraw_assets_msg( + account_addr: String, + assets_to_withdraw: Vec, +) -> CosmosMsg { + build_account_execute_warp_msgs( + account_addr, + vec![WarpMsg::WithdrawAssets(WithdrawAssetsMsg { + asset_infos: assets_to_withdraw, + })], + ) +} diff --git a/contracts/warp-resolver/Cargo.toml b/contracts/warp-resolver/Cargo.toml index 2e2456c6..1009beb8 100644 --- a/contracts/warp-resolver/Cargo.toml +++ b/contracts/warp-resolver/Cargo.toml @@ -39,7 +39,8 @@ cw-storage-plus = "0.16" cw2 = "0.16" cw20 = "0.16" cw721 = "0.16.0" -resolver = { path = "../../packages/resolver", default-features = false, version = "*" } +cw-utils = "0.16" +resolver = { path = "../../packages/resolver", default-features = false, version = "*" } controller = { path = "../../packages/controller", default-features = false, version = "*" } schemars = "0.8" thiserror = "1" diff --git a/contracts/warp-resolver/examples/warp-resolver-schema.rs b/contracts/warp-resolver/examples/warp-resolver-schema.rs index 443622f3..3abd5e69 100644 --- a/contracts/warp-resolver/examples/warp-resolver-schema.rs +++ b/contracts/warp-resolver/examples/warp-resolver-schema.rs @@ -1,6 +1,7 @@ use std::env::current_dir; use std::fs::create_dir_all; +use controller::account::WarpMsg; use cosmwasm_schema::{export_schema, remove_schemas, schema_for}; use cosmwasm_std::{CosmosMsg, QueryRequest}; use resolver::{ @@ -22,4 +23,5 @@ fn main() { export_schema(&schema_for!(SimulateResponse), &out_dir); export_schema(&schema_for!(CosmosMsg), &out_dir); export_schema(&schema_for!(QueryRequest), &out_dir); + export_schema(&schema_for!(WarpMsg), &out_dir); } diff --git a/contracts/warp-resolver/src/contract.rs b/contracts/warp-resolver/src/contract.rs index 769f65a9..951090e7 100644 --- a/contracts/warp-resolver/src/contract.rs +++ b/contracts/warp-resolver/src/contract.rs @@ -4,11 +4,12 @@ use crate::util::variable::{ vars_valid, }; use crate::ContractError; +use controller::account::{warp_msgs_to_cosmos_msgs, WarpMsg}; use cosmwasm_std::{ - entry_point, to_binary, Binary, CosmosMsg, Deps, DepsMut, Env, MessageInfo, Response, StdError, - StdResult, + entry_point, to_binary, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdError, StdResult, }; +use cw_utils::nonpayable; use resolver::condition::Condition; use resolver::variable::{QueryExpr, Variable}; use resolver::{ @@ -16,7 +17,7 @@ use resolver::{ ExecuteResolveConditionMsg, ExecuteSimulateQueryMsg, ExecuteValidateJobCreationMsg, InstantiateMsg, MigrateMsg, QueryApplyVarFnMsg, QueryHydrateMsgsMsg, QueryHydrateVarsMsg, QueryMsg, QueryResolveConditionMsg, QueryValidateJobCreationMsg, SimulateQueryMsg, - SimulateResponse, + SimulateResponse, WarpMsgsToCosmosMsgsMsg, }; #[cfg_attr(not(feature = "library"), entry_point)] @@ -36,6 +37,7 @@ pub fn execute( info: MessageInfo, msg: ExecuteMsg, ) -> Result { + nonpayable(&info).unwrap(); match msg { ExecuteMsg::ExecuteSimulateQuery(msg) => execute_simulate_query(deps, env, info, msg), ExecuteMsg::ExecuteValidateJobCreation(data) => { @@ -47,9 +49,25 @@ pub fn execute( } ExecuteMsg::ExecuteApplyVarFn(data) => execute_apply_var_fn(deps, env, info, data), ExecuteMsg::ExecuteHydrateMsgs(data) => execute_hydrate_msgs(deps, env, info, data), + ExecuteMsg::WarpMsgsToCosmosMsgs(data) => { + execute_warp_msgs_to_cosmos_msgs(deps, env, info, data) + } } } +fn execute_warp_msgs_to_cosmos_msgs( + deps: DepsMut, + env: Env, + _info: MessageInfo, + msg: WarpMsgsToCosmosMsgsMsg, +) -> Result { + let result = warp_msgs_to_cosmos_msgs(deps.as_ref(), env, msg.msgs, &msg.owner)?; + + Ok(Response::new() + .add_attribute("action", "warp_msgs_to_cosmos_msgs") + .add_attribute("response", serde_json_wasm::to_string(&result)?)) +} + pub fn execute_simulate_query( deps: DepsMut, env: Env, @@ -73,10 +91,9 @@ pub fn execute_validate_job_creation( deps.as_ref(), env, QueryValidateJobCreationMsg { - condition: data.condition, terminate_condition: data.terminate_condition, vars: data.vars, - msgs: data.msgs, + executions: data.executions, }, )?; @@ -104,6 +121,7 @@ pub fn execute_hydrate_vars( QueryHydrateVarsMsg { vars: data.vars, external_inputs: data.external_inputs, + warp_account_addr: data.warp_account_addr, }, )?; @@ -124,6 +142,7 @@ pub fn execute_resolve_condition( QueryResolveConditionMsg { condition: data.condition, vars: data.vars, + warp_account_addr: data.warp_account_addr, }, )?; @@ -144,6 +163,7 @@ pub fn execute_apply_var_fn( QueryApplyVarFnMsg { vars: data.vars, status: data.status, + warp_account_addr: data.warp_account_addr, }, )?; Ok(Response::new() @@ -192,46 +212,48 @@ fn query_validate_job_creation( _env: Env, data: QueryValidateJobCreationMsg, ) -> StdResult { - let _condition: Condition = serde_json_wasm::from_str(&data.condition) - .map_err(|e| StdError::generic_err(format!("Condition input invalid: {}", e)))?; - let terminate_condition_str = data.terminate_condition.clone().unwrap_or("".to_string()); - if !terminate_condition_str.is_empty() { - let _terminate_condition: Condition = serde_json_wasm::from_str(&terminate_condition_str) - .map_err(|e| { - StdError::generic_err(format!("Terminate condition input invalid: {}", e)) - })?; - } - let vars: Vec = serde_json_wasm::from_str(&data.vars) - .map_err(|e| StdError::generic_err(format!("Vars input invalid: {}", e)))?; + for execution in data.executions { + let _condition: Condition = serde_json_wasm::from_str(&execution.condition) + .map_err(|e| StdError::generic_err(format!("Condition input invalid: {}", e)))?; + let terminate_condition_str = data.terminate_condition.clone().unwrap_or("".to_string()); + if !terminate_condition_str.is_empty() { + let _terminate_condition: Condition = + serde_json_wasm::from_str(&terminate_condition_str).map_err(|e| { + StdError::generic_err(format!("Terminate condition input invalid: {}", e)) + })?; + } + let vars: Vec = serde_json_wasm::from_str(&data.vars) + .map_err(|e| StdError::generic_err(format!("Vars input invalid: {}", e)))?; - if !vars_valid(&vars) { - return Err(StdError::generic_err( - ContractError::InvalidVariables {}.to_string(), - )); - } + if !vars_valid(&vars) { + return Err(StdError::generic_err( + ContractError::InvalidVariables {}.to_string(), + )); + } - if has_duplicates(&vars) { - return Err(StdError::generic_err( - ContractError::VariablesContainDuplicates {}.to_string(), - )); - } + if has_duplicates(&vars) { + return Err(StdError::generic_err( + ContractError::VariablesContainDuplicates {}.to_string(), + )); + } - if !(string_vars_in_vector(&vars, &data.condition) - && string_vars_in_vector(&vars, &terminate_condition_str) - && string_vars_in_vector(&vars, &data.msgs)) - { - return Err(StdError::generic_err( - ContractError::VariablesMissingFromVector {}.to_string(), - )); - } + if !(string_vars_in_vector(&vars, &execution.condition) + && string_vars_in_vector(&vars, &terminate_condition_str) + && string_vars_in_vector(&vars, &execution.msgs)) + { + return Err(StdError::generic_err( + ContractError::VariablesMissingFromVector {}.to_string(), + )); + } - if !msgs_valid(&data.msgs, &vars).map_err(|e| StdError::generic_err(e.to_string()))? { - return Err(StdError::generic_err( - ContractError::MsgError { - msg: "msgs are invalid".to_string(), - } - .to_string(), - )); + if !msgs_valid(&execution.msgs, &vars).map_err(|e| StdError::generic_err(e.to_string()))? { + return Err(StdError::generic_err( + ContractError::MsgError { + msg: "msgs are invalid".to_string(), + } + .to_string(), + )); + } } Ok("".to_string()) @@ -240,9 +262,16 @@ fn query_validate_job_creation( fn query_hydrate_vars(deps: Deps, env: Env, data: QueryHydrateVarsMsg) -> StdResult { let vars: Vec = serde_json_wasm::from_str(&data.vars).map_err(|e| StdError::generic_err(e.to_string()))?; + serde_json_wasm::to_string( - &hydrate_vars(deps, env, vars, data.external_inputs) - .map_err(|e| StdError::generic_err(e.to_string()))?, + &hydrate_vars( + deps, + env, + vars, + data.external_inputs, + data.warp_account_addr, + ) + .map_err(|e| StdError::generic_err(e.to_string()))?, ) .map_err(|e| StdError::generic_err(e.to_string())) } @@ -257,21 +286,23 @@ fn query_resolve_condition( let vars: Vec = serde_json_wasm::from_str(&data.vars).map_err(|e| StdError::generic_err(e.to_string()))?; - resolve_cond(deps, env, condition, &vars).map_err(|e| StdError::generic_err(e.to_string())) + resolve_cond(deps, env, condition, &vars, data.warp_account_addr) + .map_err(|e| StdError::generic_err(e.to_string())) } fn query_apply_var_fn(deps: Deps, env: Env, data: QueryApplyVarFnMsg) -> StdResult { let vars: Vec = serde_json_wasm::from_str(&data.vars).map_err(|e| StdError::generic_err(e.to_string()))?; - apply_var_fn(deps, env, vars, data.status).map_err(|e| StdError::generic_err(e.to_string())) + apply_var_fn(deps, env, vars, data.status, data.warp_account_addr) + .map_err(|e| StdError::generic_err(e.to_string())) } fn query_hydrate_msgs( _deps: Deps, _env: Env, data: QueryHydrateMsgsMsg, -) -> StdResult> { +) -> StdResult> { let vars: Vec = serde_json_wasm::from_str(&data.vars).map_err(|e| StdError::generic_err(e.to_string()))?; diff --git a/contracts/warp-resolver/src/tests.rs b/contracts/warp-resolver/src/tests.rs index cff5e899..a1c9f1fa 100644 --- a/contracts/warp-resolver/src/tests.rs +++ b/contracts/warp-resolver/src/tests.rs @@ -1,37 +1,53 @@ +use controller::account::WarpMsg; +use controller::job::Execution; +use resolver::condition::{NumValue, StringEnvValue, StringValue}; use schemars::_serde_json::json; use crate::util::variable::{hydrate_msgs, hydrate_vars}; use cosmwasm_std::{testing::mock_env, WasmQuery}; -use cosmwasm_std::{to_binary, BankQuery, Binary, ContractResult, CosmosMsg, OwnedDeps, WasmMsg}; +use cosmwasm_std::{ + to_binary, BankQuery, Binary, ContractResult, CosmosMsg, OwnedDeps, Uint256, WasmMsg, +}; use crate::contract::query; use cosmwasm_schema::cw_serde; use cosmwasm_std::testing::{mock_info, MockApi, MockQuerier, MockStorage}; use cosmwasm_std::{from_slice, Empty, Querier, QueryRequest, SystemError, SystemResult}; -use resolver::variable::{QueryExpr, QueryVariable, StaticVariable, Variable, VariableKind}; +use resolver::variable::{ + FnValue, QueryExpr, QueryVariable, StaticVariable, Variable, VariableKind, +}; use resolver::{QueryMsg, QueryValidateJobCreationMsg}; use std::marker::PhantomData; +#[cw_serde] +struct TestStruct { + test: String, +} + #[test] fn test() { let deps = mock_dependencies(); let _info = mock_info("vlad", &[]); let env = mock_env(); let msg = QueryValidateJobCreationMsg { - condition: "{\"expr\":{\"decimal\":{\"op\":\"gte\",\"left\":{\"ref\":\"$warp.variable.return_amount\"},\"right\":{\"simple\":\"620000\"}}}}".parse().unwrap(), + executions: vec![Execution { + condition: "{\"expr\":{\"decimal\":{\"op\":\"gte\",\"left\":{\"ref\":\"$warp.variable.return_amount\"},\"right\":{\"simple\":\"620000\"}}}}".parse().unwrap(), + msgs: "[{\"generic\":{\"wasm\":{\"execute\":{\"contract_addr\":\"terra1fd68ah02gr2y8ze7tm9te7m70zlmc7vjyyhs6xlhsdmqqcjud4dql4wpxr\",\"msg\":\"eyJzd2FwIjp7Im9mZmVyX2Fzc2V0Ijp7ImluZm8iOnsibmF0aXZlX3Rva2VuIjp7ImRlbm9tIjoiaWJjL0IzNTA0RTA5MjQ1NkJBNjE4Q0MyOEFDNjcxQTcxRkIwOEM2Q0EwRkQwQkU3QzhBNUI1QTNFMkREOTMzQ0M5RTQifX0sImFtb3VudCI6IjEwMDAwMDAifSwibWF4X3NwcmVhZCI6IjAuNSIsImJlbGllZl9wcmljZSI6IjAuNjEwMzg3MzI3MzgyNDYzODE2In19\",\"funds\":[{\"denom\":\"ibc/B3504E092456BA618CC28AC671A71FB08C6CA0FD0BE7C8A5B5A3E2DD933CC9E4\",\"amount\":\"1000000\"}]}}}}]".to_string(), + }], terminate_condition: None, vars: "[{\"query\":{\"kind\":\"decimal\",\"name\":\"return_amount\",\"init_fn\":{\"query\":{\"wasm\":{\"smart\":{\"msg\":\"eyJzaW11bGF0aW9uIjp7Im9mZmVyX2Fzc2V0Ijp7ImFtb3VudCI6IjEwMDAwMDAiLCJpbmZvIjp7Im5hdGl2ZV90b2tlbiI6eyJkZW5vbSI6ImliYy9CMzUwNEUwOTI0NTZCQTYxOENDMjhBQzY3MUE3MUZCMDhDNkNBMEZEMEJFN0M4QTVCNUEzRTJERDkzM0NDOUU0In19fX19\",\"contract_addr\":\"terra1fd68ah02gr2y8ze7tm9te7m70zlmc7vjyyhs6xlhsdmqqcjud4dql4wpxr\"}}},\"selector\":\"$.return_amount\"},\"reinitialize\":false,\"encode\":false}}]".to_string(), - msgs: "[{\"wasm\":{\"execute\":{\"contract_addr\":\"terra1fd68ah02gr2y8ze7tm9te7m70zlmc7vjyyhs6xlhsdmqqcjud4dql4wpxr\",\"msg\":\"eyJzd2FwIjp7Im9mZmVyX2Fzc2V0Ijp7ImluZm8iOnsibmF0aXZlX3Rva2VuIjp7ImRlbm9tIjoiaWJjL0IzNTA0RTA5MjQ1NkJBNjE4Q0MyOEFDNjcxQTcxRkIwOEM2Q0EwRkQwQkU3QzhBNUI1QTNFMkREOTMzQ0M5RTQifX0sImFtb3VudCI6IjEwMDAwMDAifSwibWF4X3NwcmVhZCI6IjAuNSIsImJlbGllZl9wcmljZSI6IjAuNjEwMzg3MzI3MzgyNDYzODE2In19\",\"funds\":[{\"denom\":\"ibc/B3504E092456BA618CC28AC671A71FB08C6CA0FD0BE7C8A5B5A3E2DD933CC9E4\",\"amount\":\"1000000\"}]}}}]".to_string(), }; - let obj = serde_json_wasm::to_string(&vec!["{\"wasm\":{\"execute\":{\"contract_addr\":\"terra1fd68ah02gr2y8ze7tm9te7m70zlmc7vjyyhs6xlhsdmqqcjud4dql4wpxr\",\"msg\":\"eyJzd2FwIjp7Im9mZmVyX2Fzc2V0Ijp7ImluZm8iOnsibmF0aXZlX3Rva2VuIjp7ImRlbm9tIjoiaWJjL0IzNTA0RTA5MjQ1NkJBNjE4Q0MyOEFDNjcxQTcxRkIwOEM2Q0EwRkQwQkU3QzhBNUI1QTNFMkREOTMzQ0M5RTQifX0sImFtb3VudCI6IjEwMDAwMDAifSwibWF4X3NwcmVhZCI6IjAuNSIsImJlbGllZl9wcmljZSI6IjAuNjEwMzg3MzI3MzgyNDYzODE2In19\",\"funds\":[{\"denom\":\"ibc/B3504E092456BA618CC28AC671A71FB08C6CA0FD0BE7C8A5B5A3E2DD933CC9E4\",\"amount\":\"1000000\"}]}}}"]).unwrap(); + let obj = serde_json_wasm::to_string(&vec!["{\"generic\":{\"wasm\":{\"execute\":{\"contract_addr\":\"terra1fd68ah02gr2y8ze7tm9te7m70zlmc7vjyyhs6xlhsdmqqcjud4dql4wpxr\",\"msg\":\"eyJzd2FwIjp7Im9mZmVyX2Fzc2V0Ijp7ImluZm8iOnsibmF0aXZlX3Rva2VuIjp7ImRlbm9tIjoiaWJjL0IzNTA0RTA5MjQ1NkJBNjE4Q0MyOEFDNjcxQTcxRkIwOEM2Q0EwRkQwQkU3QzhBNUI1QTNFMkREOTMzQ0M5RTQifX0sImFtb3VudCI6IjEwMDAwMDAifSwibWF4X3NwcmVhZCI6IjAuNSIsImJlbGllZl9wcmljZSI6IjAuNjEwMzg3MzI3MzgyNDYzODE2In19\",\"funds\":[{\"denom\":\"ibc/B3504E092456BA618CC28AC671A71FB08C6CA0FD0BE7C8A5B5A3E2DD933CC9E4\",\"amount\":\"1000000\"}]}}}}"]).unwrap(); let _msg1 = QueryValidateJobCreationMsg { - condition: "{\"expr\":{\"decimal\":{\"op\":\"gte\",\"left\":{\"ref\":\"$warp.variable.return_amount\"},\"right\":{\"simple\":\"620000\"}}}}".parse().unwrap(), terminate_condition: None, vars: "[{\"query\":{\"kind\":\"decimal\",\"name\":\"return_amount\",\"init_fn\":{\"query\":{\"wasm\":{\"smart\":{\"msg\":\"eyJzaW11bGF0aW9uIjp7Im9mZmVyX2Fzc2V0Ijp7ImFtb3VudCI6IjEwMDAwMDAiLCJpbmZvIjp7Im5hdGl2ZV90b2tlbiI6eyJkZW5vbSI6ImliYy9CMzUwNEUwOTI0NTZCQTYxOENDMjhBQzY3MUE3MUZCMDhDNkNBMEZEMEJFN0M4QTVCNUEzRTJERDkzM0NDOUU0In19fX19\",\"contract_addr\":\"terra1fd68ah02gr2y8ze7tm9te7m70zlmc7vjyyhs6xlhsdmqqcjud4dql4wpxr\"}}},\"selector\":\"$.return_amount\"},\"reinitialize\":false,\"encode\":false}}]".to_string(), - msgs: obj.clone(), + executions: vec![Execution { + condition: "{\"expr\":{\"decimal\":{\"op\":\"gte\",\"left\":{\"ref\":\"$warp.variable.return_amount\"},\"right\":{\"simple\":\"620000\"}}}}".parse().unwrap(), + msgs: obj.clone(), + }], }; println!("{}", serde_json_wasm::to_string(&obj).unwrap()); @@ -128,7 +144,9 @@ fn test_hydrate_vars_nested_variables_binary_json() { kind: VariableKind::String, name: "var5".to_string(), encode: false, - value: "contract_addr".to_string(), + value: None, + init_fn: FnValue::String(StringValue::Simple("contract_addr".to_string())), + reinitialize: false, update_fn: None, }); @@ -136,7 +154,9 @@ fn test_hydrate_vars_nested_variables_binary_json() { kind: VariableKind::String, name: "var4".to_string(), encode: false, - value: "$warp.variable.var5".to_string(), + value: None, + init_fn: FnValue::String(StringValue::Ref("$warp.variable.var5".to_string())), + reinitialize: false, update_fn: None, }); @@ -189,7 +209,7 @@ fn test_hydrate_vars_nested_variables_binary_json() { }); let vars = vec![var5, var4, var3, var1, var2]; - let hydrated_vars = hydrate_vars(deps.as_ref(), env, vars, None).unwrap(); + let hydrated_vars = hydrate_vars(deps.as_ref(), env, vars, None, None).unwrap(); assert_eq!( hydrated_vars[4], @@ -221,7 +241,9 @@ fn test_hydrate_vars_nested_variables_binary() { let var1 = Variable::Static(StaticVariable { name: "var1".to_string(), kind: VariableKind::String, - value: "static_value".to_string(), + value: None, + init_fn: FnValue::String(StringValue::Simple("static_value".to_string())), + reinitialize: false, update_fn: None, encode: false, }); @@ -245,7 +267,7 @@ fn test_hydrate_vars_nested_variables_binary() { }); let vars = vec![var1, var2]; - let hydrated_vars = hydrate_vars(deps.as_ref(), env, vars, None).unwrap(); + let hydrated_vars = hydrate_vars(deps.as_ref(), env, vars, None, None).unwrap(); assert_eq!( hydrated_vars[1], @@ -274,7 +296,9 @@ fn test_hydrate_vars_nested_variables_non_binary() { let var1 = Variable::Static(StaticVariable { name: "var1".to_string(), kind: VariableKind::String, - value: "static_value".to_string(), + value: None, + init_fn: FnValue::String(StringValue::Simple("static_value".to_string())), + reinitialize: false, update_fn: None, encode: false, }); @@ -298,7 +322,7 @@ fn test_hydrate_vars_nested_variables_non_binary() { }); let vars = vec![var1, var2]; - let hydrated_vars = hydrate_vars(deps.as_ref(), env, vars, None).unwrap(); + let hydrated_vars = hydrate_vars(deps.as_ref(), env, vars, None, None).unwrap(); assert_eq!( hydrated_vars[1], @@ -328,16 +352,13 @@ fn test_hydrate_static_nested_vars_and_hydrate_msgs() { let var1 = Variable::Static(StaticVariable { name: "var1".to_string(), kind: VariableKind::String, - value: "static_value_1".to_string(), + value: None, + init_fn: FnValue::String(StringValue::Simple("static_value_1".to_string())), + reinitialize: false, update_fn: None, encode: false, }); - #[cw_serde] - struct TestStruct { - test: String, - } - // ============ TEST HYDRATED VALUE ============ let test_msg = TestStruct { @@ -351,20 +372,25 @@ fn test_hydrate_static_nested_vars_and_hydrate_msgs() { let var2 = Variable::Static(StaticVariable { name: "var2".to_string(), kind: VariableKind::String, - value: json_str.clone(), + value: None, + init_fn: FnValue::String(StringValue::Simple(json_str.clone())), + reinitialize: false, update_fn: None, // when encode is false, value will not be base64 encoded after msgs hydration encode: false, }); let vars = vec![var1.clone(), var2]; - let hydrated_vars = hydrate_vars(deps.as_ref(), env.clone(), vars, None).unwrap(); + let hydrated_vars = hydrate_vars(deps.as_ref(), env.clone(), vars, None, None).unwrap(); let hydrated_var1 = hydrated_vars[0].clone(); let hydrated_var2 = hydrated_vars[1].clone(); match hydrated_var2.clone() { Variable::Static(static_var) => { // var3.encode = false doesn't matter here, it only matters when injecting to msgs during msg hydration - assert_eq!(String::from_utf8(static_var.value.into()).unwrap(), raw_str) + assert_eq!( + String::from_utf8(static_var.value.unwrap_or_default().into()).unwrap(), + raw_str + ) } _ => panic!("Expected static variable"), }; @@ -372,19 +398,24 @@ fn test_hydrate_static_nested_vars_and_hydrate_msgs() { let var3 = Variable::Static(StaticVariable { name: "var3".to_string(), kind: VariableKind::String, - value: json_str, + value: None, + init_fn: FnValue::String(StringValue::Simple(json_str)), + reinitialize: false, update_fn: None, // when encode is true, value will be base64 encoded after msgs hydration encode: true, }); let vars = vec![var1, var3]; - let hydrated_vars = hydrate_vars(deps.as_ref(), env, vars, None).unwrap(); + let hydrated_vars = hydrate_vars(deps.as_ref(), env, vars, None, None).unwrap(); let hydrated_var3 = hydrated_vars[1].clone(); match hydrated_var3.clone() { Variable::Static(static_var) => { // var3.encode = true doesn't matter here, it only matters when injecting to msgs during msg hydration - assert_eq!(String::from_utf8(static_var.value.into()).unwrap(), raw_str); + assert_eq!( + String::from_utf8(static_var.value.unwrap_or_default().into()).unwrap(), + raw_str + ); } _ => panic!("Expected static variable"), }; @@ -403,27 +434,157 @@ fn test_hydrate_static_nested_vars_and_hydrate_msgs() { assert_eq!( hydrated_msgs[0], - CosmosMsg::Wasm(WasmMsg::Execute { + WarpMsg::Generic(CosmosMsg::Wasm(WasmMsg::Execute { // Because var1.encode = false, contract_addr should use the plain text value contract_addr: "static_value_1".to_string(), msg: Binary::from(raw_str.as_bytes()), funds: vec![] - }) + })) ); assert_eq!( hydrated_msgs[1], - CosmosMsg::Wasm(WasmMsg::Execute { + WarpMsg::Generic(CosmosMsg::Wasm(WasmMsg::Execute { // Because var3.encode = true, contract_addr should use the encoded value contract_addr: encoded_val, // msg is not Binary::from(encoded_val.as_bytes()) appears to be a cosmos msg thing, not a warp thing msg: Binary::from(raw_str.as_bytes()), funds: vec![] - }) + })) ) } #[test] -fn test_test() { - println! {"{}", "[\"{\\\"wasm\\\":{\\\"execute\\\":{\\\"contract_addr\\\":\\\"terra1na348k6rvwxje9jj6ftpsapfeyaejxjeq6tuzdmzysps20l6z23smnlv64\\\",\\\"msg\\\":\\\"eyJleGVjdXRlX3N3YXBfb3BlcmF0aW9ucyI6eyJtYXhfc3ByZWFkIjoiMC4xNSIsIm9wZXJhdGlvbnMiOlt7ImFzdHJvX3N3YXAiOnsib2ZmZXJfYXNzZXRfaW5mbyI6eyJuYXRpdmVfdG9rZW4iOnsiZGVub20iOiJ1bHVuYSJ9fSwiYXNrX2Fzc2V0X2luZm8iOnsidG9rZW4iOnsiY29udHJhY3RfYWRkciI6InRlcnJhMXhndnA2cDBxbWw1M3JlcWR5eGdjbDh0dGwwcGtoMG4ybXR4Mm43dHpmYWhuNmUwdmNhN3MwZzdzZzYifX19fSx7ImFzdHJvX3N3YXAiOnsib2ZmZXJfYXNzZXRfaW5mbyI6eyJ0b2tlbiI6eyJjb250cmFjdF9hZGRyIjoidGVycmExeGd2cDZwMHFtbDUzcmVxZHl4Z2NsOHR0bDBwa2gwbjJtdHgybjd0emZhaG42ZTB2Y2E3czBnN3NnNiJ9fSwiYXNrX2Fzc2V0X2luZm8iOnsidG9rZW4iOnsiY29udHJhY3RfYWRkciI6InRlcnJhMTY3ZHNxa2gyYWx1cng5OTd3bXljdzl5ZGt5dTU0Z3lzd2UzeWdtcnM0bHd1bWUzdm13a3M4cnVxbnYifX19fV0sIm1pbmltdW1fcmVjZWl2ZSI6IjIzNTM2NjEifX0=\\\",\\\"funds\\\":[{\\\"denom\\\":\\\"uluna\\\",\\\"amount\\\":\\\"10000\\\"}]}}}\"]".replace("\\\\", "")} +fn test_hydrate_static_env_vars_and_hydrate_msgs() { + let deps = mock_dependencies(); + let env = mock_env(); + + let dummy_warp_account_addr = "terra1".to_string(); + + let json_str = serde_json_wasm::to_string(&TestStruct { + test: format!("$warp.variable.{}", "var2"), + }) + .unwrap(); + + let raw_str = r#"{"test":"100"}"#.to_string(); + + let encoded_val = base64::encode(raw_str.clone()); + assert_eq!(encoded_val, "eyJ0ZXN0IjoiMTAwIn0="); + + // ============ TEST HYDRATED VALUE ============ + + let var1 = Variable::Static(StaticVariable { + name: "var1".to_string(), + kind: VariableKind::String, + value: None, + init_fn: FnValue::String(StringValue::Simple("static_value_1".to_string())), + reinitialize: false, + update_fn: None, + encode: false, + }); + + let var2 = Variable::Static(StaticVariable { + name: "var2".to_string(), + kind: VariableKind::Uint, + value: None, + init_fn: FnValue::Uint(NumValue::Simple(Uint256::from(100_u64))), + reinitialize: false, + update_fn: None, + encode: false, + }); + + let var3 = Variable::Static(StaticVariable { + name: "var3".to_string(), + kind: VariableKind::String, + value: None, + init_fn: FnValue::String(StringValue::Simple(json_str)), + reinitialize: false, + update_fn: None, + encode: true, + }); + + let var4 = Variable::Static(StaticVariable { + name: "var4".to_string(), + kind: VariableKind::String, + value: None, + init_fn: FnValue::String(StringValue::Env(StringEnvValue::WarpAccountAddr)), + reinitialize: false, + update_fn: None, + encode: false, + }); + + let vars = vec![var1, var2, var3, var4]; + let hydrated_vars = hydrate_vars( + deps.as_ref(), + env, + vars, + None, + Some(dummy_warp_account_addr.clone()), + ) + .unwrap(); + + let hydrated_var1 = hydrated_vars[0].clone(); + let hydrated_var2 = hydrated_vars[1].clone(); + match hydrated_var2.clone() { + Variable::Static(static_var) => { + assert_eq!( + String::from_utf8(static_var.value.unwrap_or_default().into()).unwrap(), + "100".to_string() + ) + } + _ => panic!("Expected static variable"), + }; + let hydrated_var3 = hydrated_vars[2].clone(); + match hydrated_var3.clone() { + Variable::Static(static_var) => { + assert_eq!( + String::from_utf8(static_var.value.unwrap_or_default().into()).unwrap(), + raw_str + ) + } + _ => panic!("Expected static variable"), + }; + let hydrated_var4 = hydrated_vars[3].clone(); + match hydrated_var4.clone() { + Variable::Static(static_var) => { + assert_eq!( + String::from_utf8(static_var.value.unwrap_or_default().into()).unwrap(), + dummy_warp_account_addr + ) + } + _ => panic!("Expected static variable"), + }; + + // ============ TEST HYDRATED MSG AND VAR VALUE SHOULD BE ENCODED ACCORDINGLY ============ + + let msgs = + r#"[ + {"wasm":{"execute":{"contract_addr":"$warp.variable.var1","msg":"eyJ0ZXN0IjoiMTAwIn0=","funds":[]}}}, + {"wasm":{"execute":{"contract_addr":"$warp.variable.var4","msg":"$warp.variable.var3","funds":[]}}} + ]"# + .to_string(); + + let hydrated_msgs = hydrate_msgs( + msgs, + vec![hydrated_var1, hydrated_var2, hydrated_var3, hydrated_var4], + ) + .unwrap(); + + assert_eq!( + hydrated_msgs[0], + WarpMsg::Generic(CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: "static_value_1".to_string(), + msg: Binary::from(raw_str.as_bytes()), + funds: vec![] + })) + ); + + assert_eq!( + hydrated_msgs[1], + WarpMsg::Generic(CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: dummy_warp_account_addr, + msg: Binary::from(raw_str.as_bytes()), + funds: vec![] + })) + ) } diff --git a/contracts/warp-resolver/src/util/condition.rs b/contracts/warp-resolver/src/util/condition.rs index 53641741..b2db2a75 100644 --- a/contracts/warp-resolver/src/util/condition.rs +++ b/contracts/warp-resolver/src/util/condition.rs @@ -9,7 +9,8 @@ use json_codec_wasm::ast::Ref; use json_codec_wasm::Decoder; use resolver::condition::{ BlockExpr, Condition, DecimalFnOp, Expr, GenExpr, IntFnOp, NumEnvValue, NumExprOp, - NumExprValue, NumFnValue, NumOp, NumValue, StringOp, TimeExpr, TimeOp, Value, + NumExprValue, NumFnValue, NumOp, NumValue, StringEnvValue, StringOp, StringValue, TimeExpr, + TimeOp, }; use resolver::variable::{QueryExpr, Variable}; use std::str::FromStr; @@ -19,11 +20,12 @@ pub fn resolve_cond( env: Env, cond: Condition, vars: &Vec, + warp_account_addr: Option, ) -> Result { match cond { Condition::And(conds) => { for cond in conds { - if !resolve_cond(deps, env.clone(), *cond, vars)? { + if !resolve_cond(deps, env.clone(), *cond, vars, warp_account_addr.clone())? { return Ok(false); } } @@ -31,14 +33,14 @@ pub fn resolve_cond( } Condition::Or(conds) => { for cond in conds { - if resolve_cond(deps, env.clone(), *cond, vars)? { + if resolve_cond(deps, env.clone(), *cond, vars, warp_account_addr.clone())? { return Ok(true); } } Ok(false) } - Condition::Not(cond) => Ok(!resolve_cond(deps, env, *cond, vars)?), - Condition::Expr(expr) => Ok(resolve_expr(deps, env, *expr, vars)?), + Condition::Not(cond) => Ok(!resolve_cond(deps, env, *cond, vars, warp_account_addr)?), + Condition::Expr(expr) => Ok(resolve_expr(deps, env, *expr, vars, warp_account_addr)?), } } @@ -47,9 +49,10 @@ pub fn resolve_expr( env: Env, expr: Expr, vars: &Vec, + warp_account_addr: Option, ) -> Result { match expr { - Expr::String(expr) => resolve_string_expr(deps, env, expr, vars), + Expr::String(expr) => resolve_string_expr(deps, env, expr, vars, warp_account_addr), Expr::Uint(expr) => resolve_uint_expr(deps, env, expr, vars), Expr::Int(expr) => resolve_int_expr(deps, env, expr, vars), Expr::Decimal(expr) => resolve_decimal_expr(deps, env, expr, vars), @@ -97,7 +100,9 @@ fn resolve_ref_int( let var = get_var(r, vars)?; let res = match var { Variable::Static(s) => { - let val = s.clone().value; + let val = s.clone().value.ok_or(ContractError::ConditionError { + msg: format!("Int Static value not found: {}", s.name), + })?; str::parse::(&val)? } Variable::Query(q) => { @@ -213,7 +218,9 @@ fn resolve_ref_uint( let var = get_var(r, vars)?; let res = match var { Variable::Static(s) => { - let val = s.clone().value; + let val = s.clone().value.ok_or(ContractError::ConditionError { + msg: format!("Uint Static value not found: {}", s.name), + })?; Uint256::from_str(&val)? } Variable::Query(q) => { @@ -331,7 +338,9 @@ fn resolve_ref_decimal( let var = get_var(r, vars)?; let res = match var { Variable::Static(s) => { - let val = s.clone().value; + let val = s.clone().value.ok_or(ContractError::ConditionError { + msg: format!("Decimal Static value not found: {}", s.name), + })?; Decimal256::from_str(&val)? } Variable::Query(q) => { @@ -486,34 +495,52 @@ pub fn resolve_decimal_op( pub fn resolve_string_expr( deps: Deps, env: Env, - expr: GenExpr, StringOp>, + expr: GenExpr, StringOp>, vars: &Vec, + warp_account_addr: Option, ) -> Result { - match (expr.left, expr.right) { - (Value::Simple(left), Value::Simple(right)) => { - Ok(resolve_str_op(deps, env, left, right, expr.op)) - } - (Value::Simple(left), Value::Ref(right)) => Ok(resolve_str_op( - deps, - env.clone(), - left, - resolve_ref_string(deps, env, right, vars)?, - expr.op, - )), - (Value::Ref(left), Value::Simple(right)) => Ok(resolve_str_op( - deps, - env.clone(), - resolve_ref_string(deps, env, left, vars)?, - right, - expr.op, - )), - (Value::Ref(left), Value::Ref(right)) => Ok(resolve_str_op( - deps, - env.clone(), - resolve_ref_string(deps, env.clone(), left, vars)?, - resolve_ref_string(deps, env, right, vars)?, - expr.op, - )), + let left = match expr.left { + StringValue::Simple(left) => left, + StringValue::Ref(left) => resolve_ref_string(deps, env.clone(), left, vars)?, + StringValue::Env(left) => resolve_string_value_env(deps, left, warp_account_addr.clone())?, + }; + let right = match expr.right { + StringValue::Simple(right) => right, + StringValue::Ref(right) => resolve_ref_string(deps, env.clone(), right, vars)?, + StringValue::Env(right) => resolve_string_value_env(deps, right, warp_account_addr)?, + }; + Ok(resolve_str_op(deps, env, left, right, expr.op)) +} + +pub fn resolve_string_value( + deps: Deps, + env: Env, + value: StringValue, + vars: &Vec, + warp_account_addr: Option, +) -> Result { + match value { + StringValue::Simple(value) => Ok(value), + StringValue::Ref(r) => resolve_ref_string(deps, env, r, vars), + StringValue::Env(value) => resolve_string_value_env(deps, value, warp_account_addr), + } +} + +pub fn resolve_string_value_env( + deps: Deps, + value: StringEnvValue, + warp_account_addr: Option, +) -> Result { + match value { + StringEnvValue::WarpAccountAddr => match warp_account_addr { + Some(addr) => { + deps.api.addr_validate(&addr)?; + Ok(addr) + } + None => Err(ContractError::HydrationError { + msg: "Warp account addr not found.".to_string(), + }), + }, } } @@ -525,7 +552,9 @@ fn resolve_ref_string( ) -> Result { let var = get_var(r, vars)?; let res = match var { - Variable::Static(s) => s.value.clone(), + Variable::Static(s) => s.value.clone().ok_or(ContractError::ConditionError { + msg: format!("String Static value not found: {}", s.name), + })?, Variable::Query(q) => q.value.clone().ok_or(ContractError::ConditionError { msg: format!("String Query value not found: {}", q.name), })?, @@ -591,7 +620,9 @@ pub fn resolve_ref_bool( let var = get_var(r, vars)?; let res = match var { Variable::Static(s) => { - let val = s.clone().value; + let val = s.clone().value.ok_or(ContractError::ConditionError { + msg: format!("Bool Static value not found: {}", s.name), + })?; str::parse::(&val)? } Variable::Query(q) => { diff --git a/contracts/warp-resolver/src/util/variable.rs b/contracts/warp-resolver/src/util/variable.rs index fcf29fb5..bf6dfbaf 100644 --- a/contracts/warp-resolver/src/util/variable.rs +++ b/contracts/warp-resolver/src/util/variable.rs @@ -4,6 +4,7 @@ use crate::util::condition::{ resolve_query_expr_string, resolve_query_expr_uint, resolve_ref_bool, }; use crate::ContractError; +use controller::account::WarpMsg; use cosmwasm_schema::serde::de::DeserializeOwned; use cosmwasm_schema::serde::Serialize; use cosmwasm_std::{ @@ -12,20 +13,184 @@ use cosmwasm_std::{ use std::str::FromStr; use controller::job::{ExternalInput, JobStatus}; -use resolver::variable::{QueryExpr, UpdateFnValue, Variable, VariableKind}; +use resolver::variable::{FnValue, QueryExpr, Variable, VariableKind}; + +use super::condition::resolve_string_value; pub fn hydrate_vars( deps: Deps, env: Env, vars: Vec, external_inputs: Option>, + warp_account_addr: Option, ) -> Result, ContractError> { let mut hydrated_vars = vec![]; for var in vars { let hydrated_var = match var { Variable::Static(mut v) => { - v.value = replace_in_string(v.value, &hydrated_vars)?; + if v.reinitialize || v.value.is_none() { + match v.kind { + VariableKind::Uint => match v.init_fn.clone() { + FnValue::Uint(val) => { + v.value = Some(replace_in_string( + resolve_num_value_uint(deps, env.clone(), val, &hydrated_vars)? + .to_string(), + &hydrated_vars, + )?) + } + _ => { + return Err(ContractError::HydrationError { + msg: "Variable init_fn is not of type FnValue::Uint." + .to_string(), + }) + } + }, + VariableKind::Int => match v.init_fn.clone() { + FnValue::Int(val) => { + v.value = Some(replace_in_string( + resolve_num_value_int(deps, env.clone(), val, &hydrated_vars)? + .to_string(), + &hydrated_vars, + )?) + } + _ => { + return Err(ContractError::HydrationError { + msg: "Variable init_fn is not of type FnValue::Int." + .to_string(), + }) + } + }, + VariableKind::Decimal => match v.init_fn.clone() { + FnValue::Decimal(val) => { + v.value = Some(replace_in_string( + resolve_num_value_decimal( + deps, + env.clone(), + val, + &hydrated_vars, + )? + .to_string(), + &hydrated_vars, + )?) + } + _ => { + return Err(ContractError::HydrationError { + msg: "Variable init_fn is not of type FnValue::Decimal." + .to_string(), + }) + } + }, + VariableKind::Timestamp => match v.init_fn.clone() { + FnValue::Timestamp(val) => { + v.value = Some(replace_in_string( + resolve_num_value_int(deps, env.clone(), val, &hydrated_vars)? + .to_string(), + &hydrated_vars, + )?) + } + _ => { + return Err(ContractError::HydrationError { + msg: "Variable init_fn is not of type FnValue::Timestamp." + .to_string(), + }) + } + }, + VariableKind::Bool => match v.init_fn.clone() { + FnValue::Bool(val) => { + v.value = Some(replace_in_string( + resolve_ref_bool(deps, env.clone(), val, &hydrated_vars)? + .to_string(), + &hydrated_vars, + )?) + } + _ => { + return Err(ContractError::HydrationError { + msg: "Variable init_fn is not of type FnValue::Bool." + .to_string(), + }) + } + }, + VariableKind::Amount => match v.init_fn.clone() { + FnValue::Uint(val) => { + v.value = Some(replace_in_string( + resolve_num_value_uint(deps, env.clone(), val, &hydrated_vars)? + .to_string(), + &hydrated_vars, + )?) + } + _ => { + return Err(ContractError::HydrationError { + msg: "Variable init_fn is not of type FnValue::Uint." + .to_string(), + }) + } + }, + VariableKind::String => match v.init_fn.clone() { + FnValue::String(val) => { + v.value = Some(replace_in_string( + resolve_string_value( + deps, + env.clone(), + val, + &hydrated_vars, + warp_account_addr.clone(), + )?, + &hydrated_vars, + )?) + } + _ => { + return Err(ContractError::HydrationError { + msg: "1Variable init_fn is not of type FnValue::String." + .to_string(), + }) + } + }, + VariableKind::Asset => match v.init_fn.clone() { + FnValue::String(val) => { + v.value = Some(replace_in_string( + resolve_string_value( + deps, + env.clone(), + val, + &hydrated_vars, + warp_account_addr.clone(), + )?, + &hydrated_vars, + )?) + } + _ => { + return Err(ContractError::HydrationError { + msg: "Variable init_fn is not of type FnValue::String." + .to_string(), + }) + } + }, + VariableKind::Json => match v.init_fn.clone() { + FnValue::String(val) => { + v.value = Some(replace_in_string( + resolve_string_value( + deps, + env.clone(), + val, + &hydrated_vars, + warp_account_addr.clone(), + )?, + &hydrated_vars, + )?) + } + _ => { + return Err(ContractError::HydrationError { + msg: "Variable init_fn is not of type FnValue::String." + .to_string(), + }); + } + }, + } + } + if v.value.is_none() { + return Err(ContractError::Unauthorized {}); + } Variable::Static(v) } Variable::External(mut v) => { @@ -134,7 +299,7 @@ pub fn hydrate_vars( Ok(hydrated_vars) } -pub fn hydrate_msgs(msgs: String, vars: Vec) -> Result, ContractError> { +pub fn hydrate_msgs(msgs: String, vars: Vec) -> Result, ContractError> { let mut replaced_msgs = msgs; for var in &vars { let (name, replacement) = get_replacement_in_struct(var)?; @@ -147,84 +312,104 @@ pub fn hydrate_msgs(msgs: String, vars: Vec) -> Result, } } - Ok(serde_json_wasm::from_str::>(&replaced_msgs)?) + match serde_json_wasm::from_str::>(&replaced_msgs) { + Ok(msgs) => Ok(msgs), + + // fallback to legacy flow + Err(_) => { + let msgs = serde_json_wasm::from_str::>(&replaced_msgs) + .unwrap() + .into_iter() + .map(WarpMsg::Generic) + .collect(); + + Ok(msgs) + } + } } fn get_replacement_in_struct(var: &Variable) -> Result<(String, String), ContractError> { let (name, replacement) = match var { - Variable::Static(v) => (v.name.clone(), { - match v.kind { - VariableKind::String => format!( - "\"{}\"", - match v.encode { - true => { - base64::encode(v.value.clone()) + Variable::Static(v) => match v.value.clone() { + None => { + return Err(ContractError::HydrationError { + msg: "Static msg value is none.".to_string(), + }); + } + Some(val) => (v.name.clone(), { + match v.kind { + VariableKind::Uint => format!( + "\"{}\"", + match v.encode { + true => { + base64::encode(val) + } + false => val, } - false => v.value.clone(), - } - ), - VariableKind::Uint => format!( - "\"{}\"", - match v.encode { + ), + VariableKind::Int => match v.encode { true => { - base64::encode(v.value.clone()) + format!("\"{}\"", base64::encode(val)) } - false => v.value.clone(), - } - ), - VariableKind::Int => match v.encode { - true => { - format!("\"{}\"", base64::encode(v.value.clone())) - } - false => v.value.clone(), - }, - VariableKind::Decimal => format!( - "\"{}\"", - match v.encode { + false => val, + }, + VariableKind::Decimal => format!( + "\"{}\"", + match v.encode { + true => { + base64::encode(val) + } + false => val, + } + ), + VariableKind::Timestamp => match v.encode { true => { - base64::encode(v.value.clone()) + format!("\"{}\"", base64::encode(val)) } - false => v.value.clone(), - } - ), - VariableKind::Timestamp => match v.encode { - true => { - format!("\"{}\"", base64::encode(v.value.clone())) - } - false => v.value.clone(), - }, - VariableKind::Bool => match v.encode { - true => { - format!("\"{}\"", base64::encode(v.value.clone())) - } - false => v.value.clone(), - }, - VariableKind::Amount => format!( - "\"{}\"", - match v.encode { + false => val, + }, + VariableKind::Bool => match v.encode { true => { - base64::encode(v.value.clone()) + format!("\"{}\"", base64::encode(val)) } - false => v.value.clone(), - } - ), - VariableKind::Asset => format!( - "\"{}\"", - match v.encode { + false => val, + }, + VariableKind::Amount => format!( + "\"{}\"", + match v.encode { + true => { + base64::encode(val) + } + false => val, + } + ), + VariableKind::String => format!( + "\"{}\"", + match v.encode { + true => { + base64::encode(val) + } + false => val, + } + ), + VariableKind::Asset => format!( + "\"{}\"", + match v.encode { + true => { + base64::encode(val) + } + false => val, + } + ), + VariableKind::Json => match v.encode { true => { - base64::encode(v.value.clone()) + format!("\"{}\"", base64::encode(val)) } - false => v.value.clone(), - } - ), - VariableKind::Json => match v.encode { - true => { - format!("\"{}\"", base64::encode(v.value.clone())) - } - false => v.value.clone(), - }, - } - }), + false => val, + }, + } + }), + }, Variable::External(v) => match v.value.clone() { None => { return Err(ContractError::HydrationError { @@ -313,15 +498,6 @@ fn get_replacement_in_struct(var: &Variable) -> Result<(String, String), Contrac } Some(val) => (v.name.clone(), { match v.kind { - VariableKind::String => format!( - "\"{}\"", - match v.encode { - true => { - base64::encode(val) - } - false => val, - } - ), VariableKind::Uint => format!( "\"{}\"", match v.encode { @@ -367,6 +543,15 @@ fn get_replacement_in_struct(var: &Variable) -> Result<(String, String), Contrac false => val, } ), + VariableKind::String => format!( + "\"{}\"", + match v.encode { + true => { + base64::encode(val) + } + false => val, + } + ), VariableKind::Asset => format!( "\"{}\"", match v.encode { @@ -392,13 +577,20 @@ fn get_replacement_in_struct(var: &Variable) -> Result<(String, String), Contrac fn get_replacement_in_string(var: &Variable) -> Result<(String, String), ContractError> { let (name, replacement) = match var { - Variable::Static(v) => ( - v.name.clone(), - match v.encode { - true => base64::encode(v.value.clone()), - false => v.value.clone(), - }, - ), + Variable::Static(v) => match v.value.clone() { + None => { + return Err(ContractError::HydrationError { + msg: "Static msg value is none.".to_string(), + }); + } + Some(val) => ( + v.name.clone(), + match v.encode { + true => base64::encode(val), + false => val, + }, + ), + }, Variable::External(v) => match v.value.clone() { None => { return Err(ContractError::HydrationError { @@ -571,7 +763,7 @@ pub fn msgs_valid(msgs: &str, vars: &Vec) -> Result>(&replaced_msgs)?; + let _msgs = serde_json_wasm::from_str::>(&replaced_msgs)?; Ok(true) } @@ -581,6 +773,7 @@ pub fn apply_var_fn( env: Env, vars: Vec, status: JobStatus, + warp_account_addr: Option, ) -> Result { let mut res = vec![]; for var in vars.clone() { @@ -597,122 +790,178 @@ pub fn apply_var_fn( JobStatus::Executed => match update_fn.on_success { None => (), Some(on_success) => match on_success { - UpdateFnValue::Uint(nv) => { + FnValue::Uint(nv) => { if v.kind != VariableKind::Uint { return Err(ContractError::FunctionError { msg: "Static Uint function mismatch.".to_string(), }); } - v.value = resolve_num_value_uint(deps, env.clone(), nv, &vars)? - .to_string(); + v.value = Some( + resolve_num_value_uint(deps, env.clone(), nv, &vars)? + .to_string(), + ); } - UpdateFnValue::Int(nv) => { + FnValue::Int(nv) => { if v.kind != VariableKind::Int { return Err(ContractError::FunctionError { msg: "Static Int function mismatch.".to_string(), }); } - v.value = resolve_num_value_int(deps, env.clone(), nv, &vars)? - .to_string(); + v.value = Some( + resolve_num_value_int(deps, env.clone(), nv, &vars)? + .to_string(), + ); } - UpdateFnValue::Decimal(nv) => { + FnValue::Decimal(nv) => { if v.kind != VariableKind::Decimal { return Err(ContractError::FunctionError { msg: "Static Decimal function mismatch.".to_string(), }); } - v.value = + v.value = Some( resolve_num_value_decimal(deps, env.clone(), nv, &vars)? - .to_string(); + .to_string(), + ); } - UpdateFnValue::Timestamp(nv) => { + FnValue::Timestamp(nv) => { if v.kind != VariableKind::Int { return Err(ContractError::FunctionError { msg: "Static Timestamp function mismatch.".to_string(), }); } - v.value = resolve_num_value_int(deps, env.clone(), nv, &vars)? - .to_string(); + v.value = Some( + resolve_num_value_int(deps, env.clone(), nv, &vars)? + .to_string(), + ); } - UpdateFnValue::BlockHeight(nv) => { + FnValue::BlockHeight(nv) => { if v.kind != VariableKind::Int { return Err(ContractError::FunctionError { msg: "Static BlockHeight function mismatch." .to_string(), }); } - v.value = resolve_num_value_int(deps, env.clone(), nv, &vars)? - .to_string(); + v.value = Some( + resolve_num_value_int(deps, env.clone(), nv, &vars)? + .to_string(), + ); } - UpdateFnValue::Bool(val) => { + FnValue::Bool(val) => { if v.kind != VariableKind::Bool { return Err(ContractError::FunctionError { msg: "Static Bool function mismatch.".to_string(), }); } - v.value = resolve_ref_bool(deps, env.clone(), val, &vars)? - .to_string(); + v.value = Some( + resolve_ref_bool(deps, env.clone(), val, &vars)? + .to_string(), + ); + } + FnValue::String(val) => { + if v.kind != VariableKind::String { + return Err(ContractError::FunctionError { + msg: "Static String function mismatch.".to_string(), + }); + } + v.value = Some( + resolve_string_value( + deps, + env.clone(), + val, + &vars, + warp_account_addr.clone(), + )? + .to_string(), + ); } }, }, JobStatus::Failed => match update_fn.on_error { None => (), Some(on_success) => match on_success { - UpdateFnValue::Uint(nv) => { + FnValue::Uint(nv) => { if v.kind != VariableKind::Uint { return Err(ContractError::FunctionError { msg: "Static Uint function mismatch.".to_string(), }); } - v.value = resolve_num_value_uint(deps, env.clone(), nv, &vars)? - .to_string(); + v.value = Some( + resolve_num_value_uint(deps, env.clone(), nv, &vars)? + .to_string(), + ); } - UpdateFnValue::Int(nv) => { + FnValue::Int(nv) => { if v.kind != VariableKind::Int { return Err(ContractError::FunctionError { msg: "Static Int function mismatch.".to_string(), }); } - v.value = resolve_num_value_int(deps, env.clone(), nv, &vars)? - .to_string(); + v.value = Some( + resolve_num_value_int(deps, env.clone(), nv, &vars)? + .to_string(), + ); } - UpdateFnValue::Decimal(nv) => { + FnValue::Decimal(nv) => { if v.kind != VariableKind::Decimal { return Err(ContractError::FunctionError { msg: "Static Uint function mismatch.".to_string(), }); } - v.value = + v.value = Some( resolve_num_value_decimal(deps, env.clone(), nv, &vars)? - .to_string() + .to_string(), + ); } - UpdateFnValue::Timestamp(nv) => { + FnValue::Timestamp(nv) => { if v.kind != VariableKind::Int { return Err(ContractError::FunctionError { msg: "Static Timestamp function mismatch.".to_string(), }); } - v.value = resolve_num_value_int(deps, env.clone(), nv, &vars)? - .to_string(); + v.value = Some( + resolve_num_value_int(deps, env.clone(), nv, &vars)? + .to_string(), + ); } - UpdateFnValue::BlockHeight(nv) => { + FnValue::BlockHeight(nv) => { if v.kind != VariableKind::Int { return Err(ContractError::FunctionError { msg: "Static BlockHeight function mismatch." .to_string(), }); } - v.value = resolve_num_value_int(deps, env.clone(), nv, &vars)? - .to_string(); + v.value = Some( + resolve_num_value_int(deps, env.clone(), nv, &vars)? + .to_string(), + ); } - UpdateFnValue::Bool(val) => { + FnValue::Bool(val) => { if v.kind != VariableKind::Bool { return Err(ContractError::FunctionError { msg: "Static Bool function mismatch.".to_string(), }); } - v.value = resolve_ref_bool(deps, env.clone(), val, &vars)? - .to_string(); + v.value = Some( + resolve_ref_bool(deps, env.clone(), val, &vars)? + .to_string(), + ); + } + FnValue::String(val) => { + if v.kind != VariableKind::String { + return Err(ContractError::FunctionError { + msg: "Static String function mismatch.".to_string(), + }); + } + v.value = Some( + resolve_string_value( + deps, + env.clone(), + val, + &vars, + warp_account_addr.clone(), + )? + .to_string(), + ); } }, }, @@ -737,7 +986,7 @@ pub fn apply_var_fn( JobStatus::Executed => match update_fn.on_success { None => (), Some(on_success) => match on_success { - UpdateFnValue::Uint(nv) => { + FnValue::Uint(nv) => { if v.kind != VariableKind::Uint { return Err(ContractError::FunctionError { msg: "External Uint function mismatch.".to_string(), @@ -748,7 +997,7 @@ pub fn apply_var_fn( .to_string(), ) } - UpdateFnValue::Int(nv) => { + FnValue::Int(nv) => { if v.kind != VariableKind::Int { return Err(ContractError::FunctionError { msg: "External Int function mismatch.".to_string(), @@ -759,7 +1008,7 @@ pub fn apply_var_fn( .to_string(), ) } - UpdateFnValue::Decimal(nv) => { + FnValue::Decimal(nv) => { if v.kind != VariableKind::Decimal { return Err(ContractError::FunctionError { msg: "External Decimal function mismatch.".to_string(), @@ -770,7 +1019,7 @@ pub fn apply_var_fn( .to_string(), ) } - UpdateFnValue::Timestamp(nv) => { + FnValue::Timestamp(nv) => { if v.kind != VariableKind::Int { return Err(ContractError::FunctionError { msg: "External Timestamp function mismatch." @@ -782,7 +1031,7 @@ pub fn apply_var_fn( .to_string(), ) } - UpdateFnValue::BlockHeight(nv) => { + FnValue::BlockHeight(nv) => { if v.kind != VariableKind::Int { return Err(ContractError::FunctionError { msg: "External BlockHeight function mismatch." @@ -794,7 +1043,7 @@ pub fn apply_var_fn( .to_string(), ) } - UpdateFnValue::Bool(val) => { + FnValue::Bool(val) => { if v.kind != VariableKind::Bool { return Err(ContractError::FunctionError { msg: "External Bool function mismatch.".to_string(), @@ -805,12 +1054,29 @@ pub fn apply_var_fn( .to_string(), ) } + FnValue::String(val) => { + if v.kind != VariableKind::String { + return Err(ContractError::FunctionError { + msg: "External String function mismatch.".to_string(), + }); + } + v.value = Some( + resolve_string_value( + deps, + env.clone(), + val, + &vars, + warp_account_addr.clone(), + )? + .to_string(), + ) + } }, }, JobStatus::Failed => match update_fn.on_error { None => (), Some(on_success) => match on_success { - UpdateFnValue::Uint(nv) => { + FnValue::Uint(nv) => { if v.kind != VariableKind::Uint { return Err(ContractError::FunctionError { msg: "External Uint function mismatch.".to_string(), @@ -821,7 +1087,7 @@ pub fn apply_var_fn( .to_string(), ) } - UpdateFnValue::Int(nv) => { + FnValue::Int(nv) => { if v.kind != VariableKind::Int { return Err(ContractError::FunctionError { msg: "External Int function mismatch.".to_string(), @@ -832,7 +1098,7 @@ pub fn apply_var_fn( .to_string(), ) } - UpdateFnValue::Decimal(nv) => { + FnValue::Decimal(nv) => { if v.kind != VariableKind::Decimal { return Err(ContractError::FunctionError { msg: "External Decimal function mismatch.".to_string(), @@ -843,7 +1109,7 @@ pub fn apply_var_fn( .to_string(), ) } - UpdateFnValue::Timestamp(nv) => { + FnValue::Timestamp(nv) => { if v.kind != VariableKind::Int { return Err(ContractError::FunctionError { msg: "External Timestamp function mismatch." @@ -855,7 +1121,7 @@ pub fn apply_var_fn( .to_string(), ) } - UpdateFnValue::BlockHeight(nv) => { + FnValue::BlockHeight(nv) => { if v.kind != VariableKind::Int { return Err(ContractError::FunctionError { msg: "External BlockHeight function mismatch." @@ -867,7 +1133,7 @@ pub fn apply_var_fn( .to_string(), ) } - UpdateFnValue::Bool(val) => { + FnValue::Bool(val) => { if v.kind != VariableKind::Bool { return Err(ContractError::FunctionError { msg: "External Bool function mismatch.".to_string(), @@ -878,6 +1144,23 @@ pub fn apply_var_fn( .to_string(), ) } + FnValue::String(val) => { + if v.kind != VariableKind::String { + return Err(ContractError::FunctionError { + msg: "External String function mismatch.".to_string(), + }); + } + v.value = Some( + resolve_string_value( + deps, + env.clone(), + val, + &vars, + warp_account_addr.clone(), + )? + .to_string(), + ) + } }, }, _ => { @@ -901,7 +1184,7 @@ pub fn apply_var_fn( JobStatus::Executed => match update_fn.on_success { None => (), Some(on_success) => match on_success { - UpdateFnValue::Uint(nv) => { + FnValue::Uint(nv) => { if v.kind != VariableKind::Uint { return Err(ContractError::FunctionError { msg: "Query Uint function mismatch.".to_string(), @@ -912,7 +1195,7 @@ pub fn apply_var_fn( .to_string(), ) } - UpdateFnValue::Int(nv) => { + FnValue::Int(nv) => { if v.kind != VariableKind::Int { return Err(ContractError::FunctionError { msg: "Query Int function mismatch.".to_string(), @@ -923,7 +1206,7 @@ pub fn apply_var_fn( .to_string(), ) } - UpdateFnValue::Decimal(nv) => { + FnValue::Decimal(nv) => { if v.kind != VariableKind::Decimal { return Err(ContractError::FunctionError { msg: "Query Decimal function mismatch.".to_string(), @@ -934,7 +1217,7 @@ pub fn apply_var_fn( .to_string(), ) } - UpdateFnValue::Timestamp(nv) => { + FnValue::Timestamp(nv) => { if v.kind != VariableKind::Int { return Err(ContractError::FunctionError { msg: "Query Timestamp function mismatch.".to_string(), @@ -945,7 +1228,7 @@ pub fn apply_var_fn( .to_string(), ) } - UpdateFnValue::BlockHeight(nv) => { + FnValue::BlockHeight(nv) => { if v.kind != VariableKind::Int { return Err(ContractError::FunctionError { msg: "Query Blockheighht function mismatch." @@ -957,7 +1240,7 @@ pub fn apply_var_fn( .to_string(), ) } - UpdateFnValue::Bool(val) => { + FnValue::Bool(val) => { if v.kind != VariableKind::Bool { return Err(ContractError::FunctionError { msg: "Query Bool function mismatch.".to_string(), @@ -968,12 +1251,29 @@ pub fn apply_var_fn( .to_string(), ) } + FnValue::String(val) => { + if v.kind != VariableKind::String { + return Err(ContractError::FunctionError { + msg: "Query String function mismatch.".to_string(), + }); + } + v.value = Some( + resolve_string_value( + deps, + env.clone(), + val, + &vars, + warp_account_addr.clone(), + )? + .to_string(), + ) + } }, }, JobStatus::Failed => match update_fn.on_error { None => (), Some(on_success) => match on_success { - UpdateFnValue::Uint(nv) => { + FnValue::Uint(nv) => { if v.kind != VariableKind::Uint { return Err(ContractError::FunctionError { msg: "Query Uint function mismatch.".to_string(), @@ -984,7 +1284,7 @@ pub fn apply_var_fn( .to_string(), ) } - UpdateFnValue::Int(nv) => { + FnValue::Int(nv) => { if v.kind != VariableKind::Int { return Err(ContractError::FunctionError { msg: "Query Int function mismatch.".to_string(), @@ -995,7 +1295,7 @@ pub fn apply_var_fn( .to_string(), ) } - UpdateFnValue::Decimal(nv) => { + FnValue::Decimal(nv) => { if v.kind != VariableKind::Decimal { return Err(ContractError::FunctionError { msg: "Query Decimal function mismatch.".to_string(), @@ -1006,7 +1306,7 @@ pub fn apply_var_fn( .to_string(), ) } - UpdateFnValue::Timestamp(nv) => { + FnValue::Timestamp(nv) => { if v.kind != VariableKind::Int { return Err(ContractError::FunctionError { msg: "Query Timestamp function mismatch.".to_string(), @@ -1017,7 +1317,7 @@ pub fn apply_var_fn( .to_string(), ) } - UpdateFnValue::BlockHeight(nv) => { + FnValue::BlockHeight(nv) => { if v.kind != VariableKind::Int { return Err(ContractError::FunctionError { msg: "Query BlockHeight function mismatch.".to_string(), @@ -1028,7 +1328,7 @@ pub fn apply_var_fn( .to_string(), ) } - UpdateFnValue::Bool(val) => { + FnValue::Bool(val) => { if v.kind != VariableKind::Bool { return Err(ContractError::FunctionError { msg: "Query Bool function mismatch.".to_string(), @@ -1039,6 +1339,23 @@ pub fn apply_var_fn( .to_string(), ) } + FnValue::String(val) => { + if v.kind != VariableKind::String { + return Err(ContractError::FunctionError { + msg: "Query String function mismatch.".to_string(), + }); + } + v.value = Some( + resolve_string_value( + deps, + env.clone(), + val, + &vars, + warp_account_addr.clone(), + )? + .to_string(), + ) + } }, }, _ => { @@ -1155,45 +1472,52 @@ fn get_var_name(var: &Variable) -> String { pub fn vars_valid(vars: &Vec) -> bool { for var in vars { match var { - Variable::Static(v) => match v.kind { - VariableKind::String => {} - VariableKind::Uint => { - if Uint256::from_str(&v.value).is_err() { - return false; - } - } - VariableKind::Int => { - if i128::from_str(&v.value).is_err() { - return false; - } - } - VariableKind::Decimal => { - if Decimal256::from_str(&v.value).is_err() { - return false; - } - } - VariableKind::Timestamp => { - if i128::from_str(&v.value).is_err() { - return false; - } - } - VariableKind::Bool => { - if bool::from_str(&v.value).is_err() { - return false; - } - } - VariableKind::Amount => { - if Uint128::from_str(&v.value).is_err() { - return false; - } + Variable::Static(v) => { + if v.reinitialize && v.update_fn.is_some() { + return false; } - VariableKind::Asset => { - if v.value.is_empty() { - return false; + if let Some(val) = v.value.clone() { + match v.kind { + VariableKind::String => {} + VariableKind::Uint => { + if Uint256::from_str(&val).is_err() { + return false; + } + } + VariableKind::Int => { + if i128::from_str(&val).is_err() { + return false; + } + } + VariableKind::Decimal => { + if Decimal256::from_str(&val).is_err() { + return false; + } + } + VariableKind::Timestamp => { + if i128::from_str(&val).is_err() { + return false; + } + } + VariableKind::Bool => { + if bool::from_str(&val).is_err() { + return false; + } + } + VariableKind::Amount => { + if Uint128::from_str(&val).is_err() { + return false; + } + } + VariableKind::Asset => { + if val.is_empty() { + return false; + } + } + VariableKind::Json => {} } } - VariableKind::Json => {} - }, + } Variable::External(v) => { if v.reinitialize && v.update_fn.is_some() { return false; diff --git a/contracts/warp-templates/Cargo.toml b/contracts/warp-templates/Cargo.toml index 68cf75e5..1999eb81 100644 --- a/contracts/warp-templates/Cargo.toml +++ b/contracts/warp-templates/Cargo.toml @@ -39,7 +39,7 @@ cw-storage-plus = "0.16" cw2 = "0.16" cw20 = "0.16" cw721 = "0.16.0" -templates = { path = "../../packages/templates", default-features = false, version = "*" } +templates = { path = "../../packages/templates", default-features = false, version = "*" } resolver = { path = "../../packages/resolver", default-features = false, version = "*" } schemars = "0.8" thiserror = "1" diff --git a/contracts/warp-templates/src/contract.rs b/contracts/warp-templates/src/contract.rs index 298d4776..bf12f934 100644 --- a/contracts/warp-templates/src/contract.rs +++ b/contracts/warp-templates/src/contract.rs @@ -118,10 +118,9 @@ pub fn submit_template( id: state.current_template_id, owner: info.sender.clone(), name: data.name.clone(), - msg: data.msg.clone(), + executions: data.executions.clone(), formatted_str: data.formatted_str.clone(), vars: data.vars.clone(), - condition: data.condition.clone(), }; TEMPLATES.save(deps.storage, state.current_template_id.u64(), &msg_template)?; @@ -146,7 +145,7 @@ pub fn submit_template( .add_attribute("id", state.current_template_id) .add_attribute("owner", info.sender) .add_attribute("name", data.name) - .add_attribute("msg", data.msg) + .add_attribute("executions", serde_json_wasm::to_string(&data.executions)?) .add_attribute("formatted_str", data.formatted_str) .add_attribute("vars", serde_json_wasm::to_string(&data.vars)?)) } @@ -177,10 +176,9 @@ pub fn edit_template( id: t.id, owner: t.owner, name: data.name.unwrap_or(t.name), - msg: t.msg, + executions: t.executions, formatted_str: t.formatted_str, vars: t.vars, - condition: t.condition, }), })?; @@ -189,7 +187,7 @@ pub fn edit_template( .add_attribute("id", t.id) .add_attribute("owner", info.sender) .add_attribute("name", t.name) - .add_attribute("msg", t.msg) + .add_attribute("executions", serde_json_wasm::to_string(&t.executions)?) .add_attribute("formatted_str", t.formatted_str) .add_attribute("vars", serde_json_wasm::to_string(&t.vars)?)) } diff --git a/packages/account-tracker/Cargo.toml b/packages/account-tracker/Cargo.toml new file mode 100644 index 00000000..2cef1079 --- /dev/null +++ b/packages/account-tracker/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "account-tracker" +version = "0.1.0" +authors = ["Terra Money "] +edition = "2021" + +[features] +# for more explicit tests, cargo test --features=backtraces +backtraces = ["cosmwasm-std/backtraces"] + +[dependencies] +cosmwasm-std = "1.1" +cosmwasm-schema = "1.1" + +[dev-dependencies] +cw-multi-test = "0.16" diff --git a/packages/account-tracker/README.md b/packages/account-tracker/README.md new file mode 100644 index 00000000..954383af --- /dev/null +++ b/packages/account-tracker/README.md @@ -0,0 +1,106 @@ +# CosmWasm Starter Pack + +This is a template to build smart contracts in Rust to run inside a +[Cosmos SDK](https://github.com/cosmos/cosmos-sdk) module on all chains that enable it. +To understand the framework better, please read the overview in the +[cosmwasm repo](https://github.com/CosmWasm/cosmwasm/blob/master/README.md), +and dig into the [cosmwasm docs](https://www.cosmwasm.com). +This assumes you understand the theory and just want to get coding. + +## Creating a new repo from template + +Assuming you have a recent version of rust and cargo (v1.58.1+) installed +(via [rustup](https://rustup.rs/)), +then the following should get you a new repo to start a contract: + +Install [cargo-generate](https://github.com/ashleygwilliams/cargo-generate) and cargo-run-script. +Unless you did that before, run this line now: + +```sh +cargo install cargo-generate --features vendored-openssl +cargo install cargo-run-script +``` + +Now, use it to create your new contract. +Go to the folder in which you want to place it and run: + + +**Latest: 1.0.0-beta6** + +```sh +cargo generate --git https://github.com/CosmWasm/cw-template.git --name PROJECT_NAME +```` + +**Older Version** + +Pass version as branch flag: + +```sh +cargo generate --git https://github.com/CosmWasm/cw-template.git --branch --name PROJECT_NAME +```` + +Example: + +```sh +cargo generate --git https://github.com/CosmWasm/cw-template.git --branch 0.16 --name PROJECT_NAME +``` + +You will now have a new folder called `PROJECT_NAME` (I hope you changed that to something else) +containing a simple working contract and build system that you can customize. + +## Create a Repo + +After generating, you have a initialized local git repo, but no commits, and no remote. +Go to a server (eg. github) and create a new upstream repo (called `YOUR-GIT-URL` below). +Then run the following: + +```sh +# this is needed to create a valid Cargo.lock file (see below) +cargo check +git branch -M main +git add . +git commit -m 'Initial Commit' +git remote add origin YOUR-GIT-URL +git push -u origin main +``` + +## CI Support + +We have template configurations for both [GitHub Actions](.github/workflows/Basic.yml) +and [Circle CI](.circleci/config.yml) in the generated project, so you can +get up and running with CI right away. + +One note is that the CI runs all `cargo` commands +with `--locked` to ensure it uses the exact same versions as you have locally. This also means +you must have an up-to-date `Cargo.lock` file, which is not auto-generated. +The first time you set up the project (or after adding any dep), you should ensure the +`Cargo.lock` file is updated, so the CI will test properly. This can be done simply by +running `cargo check` or `cargo unit-test`. + +## Using your project + +Once you have your custom repo, you should check out [Developing](./Developing.md) to explain +more on how to run tests and develop code. Or go through the +[online tutorial](https://docs.cosmwasm.com/) to get a better feel +of how to develop. + +[Publishing](./Publishing.md) contains useful information on how to publish your contract +to the world, once you are ready to deploy it on a running blockchain. And +[Importing](./Importing.md) contains information about pulling in other contracts or crates +that have been published. + +Please replace this README file with information about your specific project. You can keep +the `Developing.md` and `Publishing.md` files as useful referenced, but please set some +proper description in the README. + +## Gitpod integration + +[Gitpod](https://www.gitpod.io/) container-based development platform will be enabled on your project by default. + +Workspace contains: + - **rust**: for builds + - [wasmd](https://github.com/CosmWasm/wasmd): for local node setup and client + - **jq**: shell JSON manipulation tool + +Follow [Gitpod Getting Started](https://www.gitpod.io/docs/getting-started) and launch your workspace. + diff --git a/packages/account-tracker/src/lib.rs b/packages/account-tracker/src/lib.rs new file mode 100644 index 00000000..028100ab --- /dev/null +++ b/packages/account-tracker/src/lib.rs @@ -0,0 +1,201 @@ +use cosmwasm_schema::{cw_serde, QueryResponses}; +use cosmwasm_std::{Addr, Uint64}; + +#[cw_serde] +pub enum AccountType { + Funding, + Job, +} + +#[cw_serde] +pub struct Account { + pub account_type: AccountType, + pub owner_addr: Addr, + pub account_addr: Addr, +} + +#[cw_serde] +pub struct Config { + pub admin: Addr, + // Address of warp controller contract + pub warp_addr: Addr, +} + +#[cw_serde] +pub struct InstantiateMsg { + pub admin: String, + pub warp_addr: String, +} + +#[cw_serde] +#[allow(clippy::large_enum_variant)] +pub enum ExecuteMsg { + TakeJobAccount(TakeJobAccountMsg), + FreeJobAccount(FreeJobAccountMsg), + TakeFundingAccount(TakeFundingAccountMsg), + FreeFundingAccount(FreeFundingAccountMsg), + UpdateConfig(UpdateConfigMsg), +} + +#[cw_serde] +pub struct UpdateConfigMsg { + pub admin: Option, +} + +#[cw_serde] +pub struct TakeJobAccountMsg { + pub account_owner_addr: String, + pub account_addr: String, + pub job_id: Uint64, +} + +#[cw_serde] +pub struct FreeJobAccountMsg { + pub account_owner_addr: String, + pub account_addr: String, + pub last_job_id: Uint64, +} + +#[cw_serde] +pub struct TakeFundingAccountMsg { + pub account_owner_addr: String, + pub account_addr: String, + pub job_id: Uint64, +} + +#[cw_serde] +pub struct FreeFundingAccountMsg { + pub account_owner_addr: String, + pub account_addr: String, + pub job_id: Uint64, +} + +#[cw_serde] +pub struct AddFundingAccountMsg { + pub account_owner_addr: String, + pub account_addr: String, +} + +#[derive(QueryResponses)] +#[cw_serde] +pub enum QueryMsg { + #[returns(ConfigResponse)] + QueryConfig(QueryConfigMsg), + #[returns(AccountsResponse)] + QueryAccounts(QueryAccountsMsg), + #[returns(JobAccountsResponse)] + QueryJobAccounts(QueryJobAccountsMsg), + #[returns(JobAccountResponse)] + QueryJobAccount(QueryJobAccountMsg), + #[returns(JobAccountResponse)] + QueryFirstFreeJobAccount(QueryFirstFreeJobAccountMsg), + #[returns(FundingAccountsResponse)] + QueryFundingAccounts(QueryFundingAccountsMsg), + #[returns(FundingAccountResponse)] + QueryFundingAccount(QueryFundingAccountMsg), + #[returns(FundingAccountResponse)] + QueryFirstFreeFundingAccount(QueryFirstFreeFundingAccountMsg), +} + +#[cw_serde] +pub struct QueryConfigMsg {} + +#[cw_serde] +pub struct ConfigResponse { + pub config: Config, +} + +#[cw_serde] +pub struct AccountsResponse { + pub accounts: Vec, +} + +#[cw_serde] +pub struct QueryAccountsMsg { + pub account_owner_addr: String, + pub start_after: Option, + pub limit: Option, +} + +#[cw_serde] +pub enum AccountStatus { + Free, + Taken, +} + +#[cw_serde] +pub struct QueryJobAccountsMsg { + pub account_owner_addr: String, + pub account_status: AccountStatus, + pub start_after: Option, + pub limit: Option, +} + +#[cw_serde] +pub struct QueryFirstFreeJobAccountMsg { + pub account_owner_addr: String, +} + +#[cw_serde] +pub struct QueryJobAccountMsg { + pub account_owner_addr: String, + pub account_addr: String, +} + +#[cw_serde] +pub struct JobAccount { + pub account_addr: Addr, + pub taken_by_job_id: Uint64, + pub account_status: AccountStatus, +} + +#[cw_serde] +pub struct JobAccountsResponse { + pub job_accounts: Vec, + pub total_count: u32, +} + +#[cw_serde] +pub struct JobAccountResponse { + pub job_account: Option, +} + +#[cw_serde] +pub struct QueryFundingAccountMsg { + pub account_owner_addr: String, + pub account_addr: String, +} + +#[cw_serde] +pub struct QueryFirstFreeFundingAccountMsg { + pub account_owner_addr: String, +} + +#[cw_serde] +pub struct QueryFundingAccountsMsg { + pub account_owner_addr: String, + pub account_status: AccountStatus, + pub start_after: Option, + pub limit: Option, +} + +#[cw_serde] +pub struct FundingAccount { + pub account_addr: Addr, + pub taken_by_job_ids: Vec, + pub account_status: AccountStatus, +} + +#[cw_serde] +pub struct FundingAccountsResponse { + pub funding_accounts: Vec, + pub total_count: u32, +} + +#[cw_serde] +pub struct FundingAccountResponse { + pub funding_account: Option, +} + +#[cw_serde] +pub struct MigrateMsg {} diff --git a/packages/account/Cargo.toml b/packages/account/Cargo.toml index de3fb46b..1398fe84 100644 --- a/packages/account/Cargo.toml +++ b/packages/account/Cargo.toml @@ -10,20 +10,12 @@ backtraces = ["cosmwasm-std/backtraces"] [dependencies] cosmwasm-std = "1.1" -cosmwasm-storage = "1.1" cosmwasm-schema = "1.1" -cw-asset = "2.2" -cw20 = "0.16" -cw-storage-plus = "0.16" -cw2 = "0.16" schemars = "0.8" serde = { version = "1", default-features = false, features = ["derive"] } -json-codec-wasm = "0.1.0" -strum = "0.24" -strum_macros = "0.24" -thiserror = { version = "1" } -controller = {path = "../controller"} prost = "0.11.9" +controller = { path = "../controller" } + [dev-dependencies] cw-multi-test = "0.16" diff --git a/packages/account/src/lib.rs b/packages/account/src/lib.rs index 2674516b..c48c1644 100644 --- a/packages/account/src/lib.rs +++ b/packages/account/src/lib.rs @@ -1,94 +1,47 @@ -use controller::account::{AssetInfo, Fund}; -use cosmwasm_schema::cw_serde; -use cosmwasm_std::{Addr, CosmosMsg}; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; +use controller::account::{CwFund, WarpMsg, WarpMsgs}; +use cosmwasm_schema::{cw_serde, QueryResponses}; +use cosmwasm_std::{Addr, Coin as NativeCoin, Uint64}; #[cw_serde] pub struct Config { pub owner: Addr, - pub warp_addr: Addr, + // Address of warp controller contract + pub creator_addr: Addr, } #[cw_serde] pub struct InstantiateMsg { + // User who owns this account pub owner: String, - pub funds: Option>, + // ID of the job that is created along with the account + pub job_id: Uint64, + // Native funds + pub native_funds: Vec, + // CW20 or CW721 funds, will be transferred to account in reply of account instantiation + pub cw_funds: Vec, + // List of cosmos msgs to execute after instantiating the account + pub msgs: Vec, } #[cw_serde] #[allow(clippy::large_enum_variant)] pub enum ExecuteMsg { - Generic(GenericMsg), - WithdrawAssets(WithdrawAssetsMsg), - IbcTransfer(IbcTransferMsg), + WarpMsgs(WarpMsgs), } +#[derive(QueryResponses)] #[cw_serde] -pub struct GenericMsg { - pub msgs: Vec, -} - -#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, prost::Message)] -pub struct Coin { - #[prost(string, tag = "1")] - pub denom: String, - #[prost(string, tag = "2")] - pub amount: String, -} - -#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, prost::Message)] -pub struct TimeoutBlock { - #[prost(uint64, optional, tag = "1")] - pub revision_number: Option, - #[prost(uint64, optional, tag = "2")] - pub revision_height: Option, -} -#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, prost::Message)] -pub struct TransferMsg { - #[prost(string, tag = "1")] - pub source_port: String, - - #[prost(string, tag = "2")] - pub source_channel: String, - - #[prost(message, optional, tag = "3")] - pub token: Option, - - #[prost(string, tag = "4")] - pub sender: String, - - #[prost(string, tag = "5")] - pub receiver: String, - - #[prost(message, optional, tag = "6")] - pub timeout_block: Option, - - #[prost(uint64, optional, tag = "7")] - pub timeout_timestamp: Option, - - #[prost(string, tag = "8")] - pub memo: String, -} - -#[cw_serde] -pub struct IbcTransferMsg { - pub transfer_msg: TransferMsg, - pub timeout_block_delta: Option, - pub timeout_timestamp_seconds_delta: Option, -} - -#[cw_serde] -pub struct WithdrawAssetsMsg { - pub asset_infos: Vec, +pub enum QueryMsg { + #[returns(ConfigResponse)] + QueryConfig(QueryConfigMsg), } #[cw_serde] -pub struct ExecuteWasmMsg {} +pub struct QueryConfigMsg {} #[cw_serde] -pub enum QueryMsg { - Config, +pub struct ConfigResponse { + pub config: Config, } #[cw_serde] diff --git a/packages/controller/Cargo.toml b/packages/controller/Cargo.toml index 89222f6e..c41b0f7b 100644 --- a/packages/controller/Cargo.toml +++ b/packages/controller/Cargo.toml @@ -10,17 +10,15 @@ backtraces = ["cosmwasm-std/backtraces"] [dependencies] cosmwasm-std = "1.1" -cosmwasm-storage = "1.1" cosmwasm-schema = "1.1" -cw-asset = "2.2" -cw20 = "0.16" -cw-storage-plus = "0.16" -cw2 = "0.16" schemars = "0.8" serde = { version = "1", default-features = false, features = ["derive"] } strum = "0.24" strum_macros = "0.24" -thiserror = { version = "1" } +prost = "0.11.9" +cw20 = "0.16" +cw721 = "0.16.0" +serde-json-wasm = "0.4.1" [dev-dependencies] cw-multi-test = "0.16" diff --git a/packages/controller/src/account.rs b/packages/controller/src/account.rs index aa5df459..14283ae0 100644 --- a/packages/controller/src/account.rs +++ b/packages/controller/src/account.rs @@ -1,13 +1,16 @@ use cosmwasm_schema::cw_serde; -use cosmwasm_std::{Addr, Uint128}; +use cosmwasm_std::CosmosMsg::Stargate; +use cosmwasm_std::{to_binary, BankMsg, DepsMut, Uint64, WasmMsg}; +use cosmwasm_std::{Addr, CosmosMsg, Deps, Env, Response, StdError, StdResult, Uint128}; +use cw20::{BalanceResponse, Cw20ExecuteMsg}; +use cw721::{Cw721QueryMsg, OwnerOfResponse}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; -#[cw_serde] -pub struct CreateAccountMsg { - pub funds: Option>, -} +use prost::Message; #[cw_serde] -pub enum Fund { +pub enum CwFund { Cw20(Cw20Fund), Cw721(Cw721Fund), } @@ -49,35 +52,277 @@ pub enum Cw721ExecuteMsg { } #[cw_serde] -pub struct QueryAccountMsg { - pub owner: String, +pub enum AssetInfo { + Native(String), + Cw20(Addr), + Cw721(Addr, String), } #[cw_serde] -pub struct QueryAccountsMsg { - pub start_after: Option, - pub limit: Option, +pub struct WarpMsgs { + pub msgs: Vec, + pub job_id: Option, } #[cw_serde] -pub struct Account { - pub owner: Addr, - pub account: Addr, +pub enum WarpMsg { + Generic(CosmosMsg), + IbcTransfer(IbcTransferMsg), + WithdrawAssets(WithdrawAssetsMsg), } -#[cw_serde] -pub struct AccountResponse { - pub account: Account, +#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, prost::Message)] +pub struct Coin { + #[prost(string, tag = "1")] + pub denom: String, + #[prost(string, tag = "2")] + pub amount: String, +} + +#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, prost::Message)] +pub struct TimeoutBlock { + #[prost(uint64, optional, tag = "1")] + pub revision_number: Option, + #[prost(uint64, optional, tag = "2")] + pub revision_height: Option, +} +#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, prost::Message)] +pub struct TransferMsg { + #[prost(string, tag = "1")] + pub source_port: String, + + #[prost(string, tag = "2")] + pub source_channel: String, + + #[prost(message, optional, tag = "3")] + pub token: Option, + + #[prost(string, tag = "4")] + pub sender: String, + + #[prost(string, tag = "5")] + pub receiver: String, + + #[prost(message, optional, tag = "6")] + pub timeout_block: Option, + + #[prost(uint64, optional, tag = "7")] + pub timeout_timestamp: Option, + + #[prost(string, tag = "8")] + pub memo: String, } #[cw_serde] -pub struct AccountsResponse { - pub accounts: Vec, +pub struct IbcTransferMsg { + pub transfer_msg: TransferMsg, + pub timeout_block_delta: Option, + pub timeout_timestamp_seconds_delta: Option, } #[cw_serde] -pub enum AssetInfo { - Native(String), - Cw20(Addr), - Cw721(Addr, String), +pub struct WithdrawAssetsMsg { + pub asset_infos: Vec, +} + +pub fn execute_warp_msgs( + deps: DepsMut, + env: Env, + data: WarpMsgs, + owner: &Addr, +) -> Result { + let msgs = warp_msgs_to_cosmos_msgs(deps.as_ref(), env, data.msgs, owner).unwrap(); + + let mut resp = Response::new() + .add_messages(msgs) + .add_attribute("action", "warp_msgs"); + + if let Some(job_id) = data.job_id { + resp = resp.add_attribute("job_id", job_id); + } + + Ok(resp) +} + +pub fn warp_msgs_to_cosmos_msgs( + deps: Deps, + env: Env, + msgs: Vec, + owner: &Addr, +) -> Result, StdError> { + let result = msgs + .into_iter() + .flat_map(|msg| -> Vec { + match msg { + WarpMsg::Generic(msg) => vec![msg], + WarpMsg::IbcTransfer(msg) => ibc_transfer(env.clone(), msg) + .map(extract_messages) + .unwrap(), + WarpMsg::WithdrawAssets(msg) => withdraw_assets(deps, env.clone(), msg, owner) + .map(extract_messages) + .unwrap(), + } + }) + .collect::>(); + + Ok(result) +} + +fn extract_messages(resp: Response) -> Vec { + resp.messages + .into_iter() + .map(|cosmos_msg| cosmos_msg.msg) + .collect() +} + +pub fn withdraw_assets( + deps: Deps, + env: Env, + data: WithdrawAssetsMsg, + owner: &Addr, +) -> Result { + let mut withdraw_msgs: Vec = vec![]; + + for asset_info in &data.asset_infos { + match asset_info { + AssetInfo::Native(denom) => { + let withdraw_native_msg = withdraw_asset_native(deps, env.clone(), owner, denom)?; + + match withdraw_native_msg { + None => {} + Some(msg) => withdraw_msgs.push(msg), + } + } + AssetInfo::Cw20(addr) => { + let withdraw_cw20_msg = withdraw_asset_cw20(deps, env.clone(), owner, addr)?; + + match withdraw_cw20_msg { + None => {} + Some(msg) => withdraw_msgs.push(msg), + } + } + AssetInfo::Cw721(addr, token_id) => { + let withdraw_cw721_msg = withdraw_asset_cw721(deps, owner, addr, token_id)?; + match withdraw_cw721_msg { + None => {} + Some(msg) => withdraw_msgs.push(msg), + } + } + } + } + + Ok(Response::new() + .add_messages(withdraw_msgs) + .add_attribute("action", "withdraw_assets") + .add_attribute( + "assets", + serde_json_wasm::to_string(&data.asset_infos).unwrap(), + )) +} + +fn withdraw_asset_native( + deps: Deps, + env: Env, + owner: &Addr, + denom: &String, +) -> StdResult> { + let amount = deps.querier.query_balance(env.contract.address, denom)?; + + let res = if amount.amount > Uint128::zero() { + Some(CosmosMsg::Bank(BankMsg::Send { + to_address: owner.to_string(), + amount: vec![amount], + })) + } else { + None + }; + + Ok(res) +} + +fn withdraw_asset_cw20( + deps: Deps, + env: Env, + owner: &Addr, + token: &Addr, +) -> StdResult> { + let amount: BalanceResponse = deps.querier.query_wasm_smart( + token.to_string(), + &cw20::Cw20QueryMsg::Balance { + address: env.contract.address.to_string(), + }, + )?; + + let res = if amount.balance > Uint128::zero() { + Some(CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: token.to_string(), + msg: to_binary(&Cw20ExecuteMsg::Transfer { + recipient: owner.to_string(), + amount: amount.balance, + })?, + funds: vec![], + })) + } else { + None + }; + + Ok(res) +} + +fn withdraw_asset_cw721( + deps: Deps, + owner: &Addr, + token: &Addr, + token_id: &String, +) -> StdResult> { + let owner_query: OwnerOfResponse = deps.querier.query_wasm_smart( + token.to_string(), + &Cw721QueryMsg::OwnerOf { + token_id: token_id.to_string(), + include_expired: None, + }, + )?; + + let res = if owner_query.owner == *owner { + Some(CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: token.to_string(), + msg: to_binary(&Cw721ExecuteMsg::TransferNft { + recipient: owner.to_string(), + token_id: token_id.to_string(), + })?, + funds: vec![], + })) + } else { + None + }; + + Ok(res) +} + +pub fn ibc_transfer(env: Env, data: IbcTransferMsg) -> Result { + let mut transfer_msg = data.transfer_msg.clone(); + + if data.timeout_block_delta.is_some() && data.transfer_msg.timeout_block.is_some() { + let block = transfer_msg.timeout_block.unwrap(); + transfer_msg.timeout_block = Some(TimeoutBlock { + revision_number: Some(block.revision_number()), + revision_height: Some(env.block.height + data.timeout_block_delta.unwrap()), + }) + } + + if data.timeout_timestamp_seconds_delta.is_some() { + transfer_msg.timeout_timestamp = Some( + env.block + .time + .plus_seconds( + env.block.time.seconds() + data.timeout_timestamp_seconds_delta.unwrap(), + ) + .nanos(), + ); + } + + Ok(Response::new().add_message(Stargate { + type_url: "/ibc.applications.transfer.v1.MsgTransfer".to_string(), + value: transfer_msg.encode_to_vec().into(), + })) } diff --git a/packages/controller/src/job.rs b/packages/controller/src/job.rs index 37939f30..42969271 100644 --- a/packages/controller/src/job.rs +++ b/packages/controller/src/job.rs @@ -1,40 +1,39 @@ -use crate::account::AssetInfo; +use crate::account::{AssetInfo, CwFund, WarpMsg}; use cosmwasm_schema::cw_serde; use cosmwasm_std::{Addr, Uint128, Uint64}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use strum_macros::Display; -// pub enum JobFund { -// Cw20(...), -// Native(...), -// Ibc(...) -// } - -// 1. create_account (can potential embed funds here) -// 2. cw20_sends, native (native send or within the create_job msg itself), ibc_send (to account) -// 3. create_job msg -// - job.funds -> withdraw_asset_from_account(...), withdraws from account to controller contract -// ... -// 4. execute_job msg -// - job succceeded - - #[cw_serde] pub struct Job { pub id: Uint64, + // Exist if job is the follow up job of a recurring job + pub prev_id: Option, pub owner: Addr, + // Warp account this job is associated with, job will be executed in the context of it and + // pay protocol fee from it. As job creator can have infinite job accounts, each job account + // can only be used by up to 1 active job, so each job's fund is isolated + pub account: Addr, + // Funding account from which job fees and rewards are deducted. + // - required for recurring jobs + // - optionally provided for one time jobs + pub funding_account: Option, pub last_update_time: Uint64, pub name: String, pub description: String, pub labels: Vec, pub status: JobStatus, - pub condition: String, pub terminate_condition: Option, - pub msgs: String, + pub executions: Vec, pub vars: String, pub recurring: bool, - pub requeue_on_evict: bool, + pub duration_days: Uint64, + pub created_at_time: Uint64, pub reward: Uint128, + // Acts like a lifecycle method - called on job termination. + // For withdrawing assets on each job execution (recurring jobs), + // use WithdrawAssets warp msg pub assets_to_withdraw: Vec, } @@ -53,19 +52,38 @@ pub enum JobStatus { Evicted, } +#[cw_serde] +pub struct Execution { + pub condition: String, + pub msgs: String, +} + #[cw_serde] pub struct CreateJobMsg { pub name: String, pub description: String, pub labels: Vec, - pub condition: String, + // exit condition for recurring jobs pub terminate_condition: Option, - pub msgs: String, + pub executions: Vec, pub vars: String, pub recurring: bool, - pub requeue_on_evict: bool, pub reward: Uint128, + // without funding account: operational_amount needs to equal total_fees + reward + // with funding account: ignored, can be set to 0 + pub operational_amount: Uint128, + pub duration_days: Uint64, + // Acts like a lifecycle method - called on job termination. + // For withdrawing assets on each job execution (recurring jobs), + // use WithdrawAssets warp msg pub assets_to_withdraw: Option>, + // messages that are executed via job-account when the job is created + pub account_msgs: Option>, + pub cw_funds: Option>, + // Funding account from which job fees and rewards are deducted. + // - required for recurring jobs + // - optionally provided for one time jobs + pub funding_account: Option, } #[cw_serde] @@ -79,7 +97,6 @@ pub struct UpdateJobMsg { pub name: Option, pub description: Option, pub labels: Option>, - pub added_reward: Option, } #[cw_serde] @@ -146,5 +163,5 @@ pub struct JobResponse { #[cw_serde] pub struct JobsResponse { pub jobs: Vec, - pub total_count: usize, + pub total_count: u32, } diff --git a/packages/controller/src/lib.rs b/packages/controller/src/lib.rs index 0ab1707f..23eb3073 100644 --- a/packages/controller/src/lib.rs +++ b/packages/controller/src/lib.rs @@ -1,6 +1,3 @@ -use crate::account::{ - AccountResponse, AccountsResponse, CreateAccountMsg, QueryAccountMsg, QueryAccountsMsg, -}; use crate::job::{ CreateJobMsg, DeleteJobMsg, EvictJobMsg, ExecuteJobMsg, JobResponse, JobsResponse, QueryJobMsg, QueryJobsMsg, UpdateJobMsg, @@ -19,19 +16,25 @@ pub struct Config { pub fee_collector: Addr, pub warp_account_code_id: Uint64, pub minimum_reward: Uint128, - pub creation_fee_percentage: Uint64, - pub cancellation_fee_percentage: Uint64, + pub cancellation_fee_rate: Uint64, + // By querying job account tracker contract + // We know all accounts owned by that user and each account's availability + // For more detail, please refer to job account tracker contract + pub account_tracker_address: Addr, pub resolver_address: Addr, - // maximum time for evictions - pub t_max: Uint64, - // minimum time for evictions - pub t_min: Uint64, - // maximum fee for evictions - pub a_max: Uint128, - // minimum fee for evictions - pub a_min: Uint128, - // maximum length of queue modifier for evictions - pub q_max: Uint64, + pub creation_fee_min: Uint128, + pub creation_fee_max: Uint128, + pub burn_fee_min: Uint128, + pub maintenance_fee_min: Uint128, + pub maintenance_fee_max: Uint128, + // duration_days fn interval [left, right] + pub duration_days_min: Uint64, + pub duration_days_max: Uint64, + pub duration_days_limit: Uint64, + // queue_size fn interval [left, right] + pub queue_size_left: Uint64, + pub queue_size_right: Uint64, + pub burn_fee_rate: Uint128, } #[cw_serde] @@ -48,19 +51,28 @@ pub struct InstantiateMsg { pub fee_denom: String, pub fee_collector: Option, pub warp_account_code_id: Uint64, + pub account_tracker_code_id: Uint64, pub minimum_reward: Uint128, - pub creation_fee: Uint64, - pub cancellation_fee: Uint64, + pub cancellation_fee_rate: Uint64, pub resolver_address: String, - pub t_max: Uint64, - pub t_min: Uint64, - pub a_max: Uint128, - pub a_min: Uint128, - pub q_max: Uint64, + pub creation_fee_min: Uint128, + pub creation_fee_max: Uint128, + pub burn_fee_min: Uint128, + pub maintenance_fee_min: Uint128, + pub maintenance_fee_max: Uint128, + // duration_days fn interval [left, right] + pub duration_days_min: Uint64, + pub duration_days_max: Uint64, + pub duration_days_limit: Uint64, + // queue_size fn interval [left, right] + pub queue_size_left: Uint64, + pub queue_size_right: Uint64, + pub burn_fee_rate: Uint128, } //execute #[cw_serde] +#[allow(clippy::large_enum_variant)] pub enum ExecuteMsg { CreateJob(CreateJobMsg), DeleteJob(DeleteJobMsg), @@ -68,13 +80,14 @@ pub enum ExecuteMsg { ExecuteJob(ExecuteJobMsg), EvictJob(EvictJobMsg), - CreateAccount(CreateAccountMsg), - UpdateConfig(UpdateConfigMsg), MigrateAccounts(MigrateAccountsMsg), + MigratePendingJobs(MigrateJobsMsg), MigrateFinishedJobs(MigrateJobsMsg), + + CreateFundingAccount(CreateFundingAccountMsg), } #[cw_serde] @@ -82,17 +95,25 @@ pub struct UpdateConfigMsg { pub owner: Option, pub fee_collector: Option, pub minimum_reward: Option, - pub creation_fee_percentage: Option, - pub cancellation_fee_percentage: Option, - pub t_max: Option, - pub t_min: Option, - pub a_max: Option, - pub a_min: Option, - pub q_max: Option, + pub cancellation_fee_rate: Option, + pub creation_fee_min: Option, + pub creation_fee_max: Option, + pub burn_fee_min: Option, + pub maintenance_fee_min: Option, + pub maintenance_fee_max: Option, + // duration_days fn interval [left, right] + pub duration_days_min: Option, + pub duration_days_max: Option, + pub duration_days_limit: Option, + // queue_size fn interval [left, right] + pub queue_size_left: Option, + pub queue_size_right: Option, + pub burn_fee_rate: Option, } #[cw_serde] pub struct MigrateAccountsMsg { + pub account_owner_addr: String, pub warp_account_code_id: Uint64, pub start_after: Option, pub limit: u8, @@ -104,6 +125,9 @@ pub struct MigrateJobsMsg { pub limit: u8, } +#[cw_serde] +pub struct CreateFundingAccountMsg {} + //query #[derive(QueryResponses)] #[cw_serde] @@ -113,24 +137,28 @@ pub enum QueryMsg { #[returns(JobsResponse)] QueryJobs(QueryJobsMsg), - #[returns(AccountResponse)] - QueryAccount(QueryAccountMsg), - #[returns(AccountsResponse)] - QueryAccounts(QueryAccountsMsg), - #[returns(ConfigResponse)] QueryConfig(QueryConfigMsg), + + #[returns(StateResponse)] + QueryState(QueryStateMsg), } #[cw_serde] pub struct QueryConfigMsg {} -//responses #[cw_serde] pub struct ConfigResponse { pub config: Config, } -//migrate//{"resolver_address":"terra1a8dxkrapwj4mkpfnrv7vahd0say0lxvd0ft6qv","warp_account_code_id":"10081"} +#[cw_serde] +pub struct QueryStateMsg {} + +#[cw_serde] +pub struct StateResponse { + pub state: State, +} + #[cw_serde] pub struct MigrateMsg {} diff --git a/packages/resolver/Cargo.toml b/packages/resolver/Cargo.toml index 704870a3..aefa9005 100644 --- a/packages/resolver/Cargo.toml +++ b/packages/resolver/Cargo.toml @@ -10,19 +10,8 @@ backtraces = ["cosmwasm-std/backtraces"] [dependencies] cosmwasm-std = "1.1" -cosmwasm-storage = "1.1" cosmwasm-schema = "1.1" -cw-asset = "2.2" -cw20 = "0.16" -cw-storage-plus = "0.16" -cw2 = "0.16" -schemars = "0.8" -serde = { version = "1", default-features = false, features = ["derive"] } -json-codec-wasm = "0.1.0" -strum = "0.24" -strum_macros = "0.24" -thiserror = { version = "1" } -controller = {path = "../controller"} +controller = { path = "../controller" } [dev-dependencies] cw-multi-test = "0.16" diff --git a/packages/resolver/src/condition.rs b/packages/resolver/src/condition.rs index c3932529..1056b756 100644 --- a/packages/resolver/src/condition.rs +++ b/packages/resolver/src/condition.rs @@ -31,9 +31,15 @@ pub struct BlockExpr { } #[cw_serde] -pub enum Value { +pub enum StringValue { Simple(T), Ref(String), + Env(StringEnvValue), +} + +#[cw_serde] +pub enum StringEnvValue { + WarpAccountAddr, } #[cw_serde] @@ -90,7 +96,7 @@ pub enum IntFnOp { #[cw_serde] pub enum Expr { - String(GenExpr, StringOp>), + String(GenExpr, StringOp>), Uint(GenExpr, NumOp>), Int(GenExpr, NumOp>), Decimal(GenExpr, NumOp>), diff --git a/packages/resolver/src/lib.rs b/packages/resolver/src/lib.rs index 8030e6ea..34f76f74 100644 --- a/packages/resolver/src/lib.rs +++ b/packages/resolver/src/lib.rs @@ -1,9 +1,12 @@ pub mod condition; pub mod variable; -use controller::job::{ExternalInput, JobStatus}; +use controller::{ + account::WarpMsg, + job::{Execution, ExternalInput, JobStatus}, +}; use cosmwasm_schema::{cw_serde, QueryResponses}; -use cosmwasm_std::{CosmosMsg, QueryRequest}; +use cosmwasm_std::{Addr, QueryRequest}; #[cw_serde] pub struct InstantiateMsg {} @@ -15,6 +18,7 @@ pub enum ExecuteMsg { ExecuteResolveCondition(ExecuteResolveConditionMsg), ExecuteApplyVarFn(ExecuteApplyVarFnMsg), ExecuteHydrateMsgs(ExecuteHydrateMsgsMsg), + WarpMsgsToCosmosMsgs(WarpMsgsToCosmosMsgsMsg), } #[derive(QueryResponses)] @@ -30,13 +34,19 @@ pub enum QueryMsg { QueryResolveCondition(QueryResolveConditionMsg), #[returns(String)] QueryApplyVarFn(QueryApplyVarFnMsg), - #[returns(Vec)] + #[returns(Vec)] QueryHydrateMsgs(QueryHydrateMsgsMsg), } #[cw_serde] pub struct MigrateMsg {} +#[cw_serde] +pub struct WarpMsgsToCosmosMsgsMsg { + pub msgs: Vec, + pub owner: Addr, +} + #[cw_serde] pub struct ExecuteSimulateQueryMsg { pub query: QueryRequest, @@ -52,34 +62,35 @@ pub struct ExecuteHydrateMsgsMsg { pub struct ExecuteHydrateVarsMsg { pub vars: String, pub external_inputs: Option>, + pub warp_account_addr: Option, } #[cw_serde] pub struct ExecuteResolveConditionMsg { pub condition: String, pub vars: String, + pub warp_account_addr: Option, } #[cw_serde] pub struct ExecuteApplyVarFnMsg { pub vars: String, pub status: JobStatus, + pub warp_account_addr: Option, } #[cw_serde] pub struct ExecuteValidateJobCreationMsg { - pub condition: String, pub terminate_condition: Option, pub vars: String, - pub msgs: String, + pub executions: Vec, } #[cw_serde] pub struct QueryValidateJobCreationMsg { - pub condition: String, pub terminate_condition: Option, pub vars: String, - pub msgs: String, + pub executions: Vec, } #[cw_serde] @@ -92,18 +103,21 @@ pub struct QueryHydrateMsgsMsg { pub struct QueryHydrateVarsMsg { pub vars: String, pub external_inputs: Option>, + pub warp_account_addr: Option, } #[cw_serde] pub struct QueryResolveConditionMsg { pub condition: String, pub vars: String, + pub warp_account_addr: Option, } #[cw_serde] pub struct QueryApplyVarFnMsg { pub vars: String, pub status: JobStatus, + pub warp_account_addr: Option, } #[cw_serde] diff --git a/packages/resolver/src/variable.rs b/packages/resolver/src/variable.rs index 97e1bb9d..b8cf1228 100644 --- a/packages/resolver/src/variable.rs +++ b/packages/resolver/src/variable.rs @@ -3,6 +3,8 @@ use std::collections::HashMap; use cosmwasm_schema::cw_serde; use cosmwasm_std::{Decimal256, QueryRequest, Uint256}; +use crate::condition::StringValue; + use super::condition::{DecimalFnOp, IntFnOp, NumExprOp, NumValue}; #[cw_serde] @@ -68,19 +70,20 @@ pub enum FnOp { } #[cw_serde] -pub enum UpdateFnValue { +pub enum FnValue { Uint(NumValue), Int(NumValue), Decimal(NumValue), Timestamp(NumValue), BlockHeight(NumValue), Bool(String), //ref + String(StringValue), } #[cw_serde] pub struct UpdateFn { - pub on_success: Option, - pub on_error: Option, + pub on_success: Option, + pub on_error: Option, } // Variable is specified as a reference value (string) in form of $warp.variable.{name} @@ -97,7 +100,9 @@ pub struct StaticVariable { pub kind: VariableKind, pub name: String, pub encode: bool, - pub value: String, + pub init_fn: FnValue, + pub reinitialize: bool, + pub value: Option, //none if uninitialized pub update_fn: Option, } diff --git a/packages/templates/Cargo.toml b/packages/templates/Cargo.toml index 919276d5..02e1f5f8 100644 --- a/packages/templates/Cargo.toml +++ b/packages/templates/Cargo.toml @@ -10,20 +10,9 @@ backtraces = ["cosmwasm-std/backtraces"] [dependencies] cosmwasm-std = "1.1" -cosmwasm-storage = "1.1" cosmwasm-schema = "1.1" -cw-asset = "2.2" -cw20 = "0.16" -cw-storage-plus = "0.16" -cw2 = "0.16" -schemars = "0.8" -serde = { version = "1", default-features = false, features = ["derive"] } -resolver = { path = "../resolver", default-features = false, version = "*" } -json-codec-wasm = "0.1.0" -strum = "0.24" -strum_macros = "0.24" -thiserror = { version = "1" } -controller = {path = "../controller"} +controller = { path = "../controller" } +resolver = { path = "../resolver" } [dev-dependencies] cw-multi-test = "0.16" diff --git a/packages/templates/src/template.rs b/packages/templates/src/template.rs index 90f6ed32..0806f49d 100644 --- a/packages/templates/src/template.rs +++ b/packages/templates/src/template.rs @@ -1,6 +1,6 @@ +use controller::job::Execution; use cosmwasm_schema::cw_serde; use cosmwasm_std::{Addr, Uint64}; -use resolver::condition::Condition; use resolver::variable::Variable; //msg templates @@ -10,16 +10,14 @@ pub struct Template { pub owner: Addr, pub name: String, pub vars: Vec, - pub msg: String, - pub condition: Option, + pub executions: Vec, pub formatted_str: String, } #[cw_serde] pub struct SubmitTemplateMsg { pub name: String, - pub msg: String, - pub condition: Option, + pub executions: Vec, pub formatted_str: String, pub vars: Vec, } diff --git a/refs.json b/refs.json index e7c38b7d..f662f32d 100644 --- a/refs.json +++ b/refs.json @@ -9,19 +9,23 @@ }, "testnet": { "warp-account": { - "codeId": "12863" + "codeId": "12858" }, "warp-controller": { - "codeId": "12866", - "address": "terra1st4hsvusy8k3cfmvqwlxfx0cyxtn237qpfscg8q9yd8gl07mz5kqh7lste" + "codeId": "12861", + "address": "terra1mmsl3mxq9n8a6dgye05pn0qlup7r24e2vyjkqgpe32pv3ehjgnes0jz5nc" }, "warp-resolver": { - "codeId": "12864", - "address": "terra1unp8qs827plupty8kef9w5duwr9prmtdtajqjrgkl3uxad7ped9qt97zcu" + "codeId": "12859", + "address": "terra1kjv3e7v7m03kk8lrjqr2j604vusxrpxadg6xjz89jucladh5m5gqqag8q7" }, "warp-templates": { - "codeId": "12865", - "address": "terra1nc8xuc0yp0kmdx703slelmyd2p2yawr27ppjqanx4wh0upm4yhuqkyx0y5" + "codeId": "12860", + "address": "terra155wp5wwvquqzg30r6luu4e9d95p7pexe3xjszhflcsqe5gpayd6smz5w6k" + }, + "warp-account-tracker": { + "codeId": "12862", + "address": "terra15yefd9r33wad527jrxphef8r0jr7n4chg4ehgq0lmrwsfsflaajq5ps2jz" } }, "mainnet": { diff --git a/tasks/deploy_warp.ts b/tasks/deploy_warp.ts index 31dbe0a0..135f887f 100644 --- a/tasks/deploy_warp.ts +++ b/tasks/deploy_warp.ts @@ -1,11 +1,10 @@ import task from "@terra-money/terrariums"; task(async ({ deployer, signer, refs }) => { - //account - deployer.buildContract("warp-account"); - deployer.optimizeContract("warp-account"); + deployer.buildContract("warp-controller"); + deployer.optimizeContract("warp-controller"); - const id = await deployer.storeCode("warp-account"); + const account_contract_id = await deployer.storeCode("warp-account"); await new Promise((resolve) => setTimeout(resolve, 10000)); await deployer.storeCode("warp-resolver"); @@ -17,35 +16,49 @@ task(async ({ deployer, signer, refs }) => { await deployer.storeCode("warp-controller"); await new Promise((resolve) => setTimeout(resolve, 10000)); + const account_tracker_id = await deployer.storeCode("warp-account-tracker"); + await new Promise((resolve) => setTimeout(resolve, 10000)); + const instantiateTemplatesMsg = { owner: signer.key.accAddress, fee_collector: signer.key.accAddress, templates: [], fee_denom: "uluna", - } + }; await deployer.instantiate("warp-templates", instantiateTemplatesMsg, { admin: signer.key.accAddress, }); await new Promise((resolve) => setTimeout(resolve, 10000)); - let resolver_address = await deployer.instantiate("warp-resolver", {}, { - admin: signer.key.accAddress, - }); + let resolver_address = await deployer.instantiate( + "warp-resolver", + {}, + { + admin: signer.key.accAddress, + } + ); await new Promise((resolve) => setTimeout(resolve, 10000)); const instantiateControllerMsg = { - warp_account_code_id: id, fee_denom: "uluna", - creation_fee: "5", - cancellation_fee: "5", - minimum_reward: "10000", - resolver_address: resolver_address.address, - t_max: "86400", - t_min: "86400", - a_max: "10000", - a_min: "10000", - q_max: "10", + fee_collector: signer.key.accAddress, + warp_account_code_id: account_contract_id, + account_tracker_code_id: account_tracker_id, + minimum_reward: "100000", // 0.1 LUNA + cancellation_fee_rate: "5", + resolver_address: resolver_address, + creation_fee_min: "500000", // 0.5 LUNA + creation_fee_max: "100000000", // 100 LUNA + burn_fee_min: "250000", // 0.25 LUNA + maintenance_fee_min: "250000", // 0.25 LUNA + maintenance_fee_max: "10000000", // 10 LUNA + duration_days_min: "7", + duration_days_max: "90", + duration_days_limit: "180", + queue_size_left: "5000", + queue_size_right: "50000", + burn_fee_rate: "25", // 25% of job reward }; await deployer.instantiate("warp-controller", instantiateControllerMsg, {