diff --git a/.circleci/config.yml b/.circleci/config.yml index 6cab49051..5c14dbe71 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -8,6 +8,7 @@ workflows: - contract_cw20_base - contract_cw20_escrow - package_cw1 + - package_cw2 - package_cw20 - lint @@ -176,10 +177,10 @@ jobs: - target key: cargocache-cw20-escrow-rust:1.44.1-{{ checksum "~/project/Cargo.lock" }} - package_cw20: + package_cw1: docker: - image: rust:1.44.1 - working_directory: ~/project/packages/cw20 + working_directory: ~/project/packages/cw1 steps: - checkout: path: ~/project @@ -188,7 +189,7 @@ jobs: command: rustc --version; cargo --version; rustup --version; rustup target list --installed - restore_cache: keys: - - cargocache-v2-cw20:1.44.1-{{ checksum "~/project/Cargo.lock" }} + - cargocache-v2-cw1:1.44.1-{{ checksum "~/project/Cargo.lock" }} - run: name: Add wasm32 target command: rustup target add wasm32-unknown-unknown && rustup target list --installed @@ -217,12 +218,12 @@ jobs: paths: - /usr/local/cargo/registry - target - key: cargocache-v2-cw20:1.44.1-{{ checksum "~/project/Cargo.lock" }} + key: cargocache-v2-cw1:1.44.1-{{ checksum "~/project/Cargo.lock" }} - package_cw1: + package_cw2: docker: - image: rust:1.44.1 - working_directory: ~/project/packages/cw1 + working_directory: ~/project/packages/cw2 steps: - checkout: path: ~/project @@ -231,7 +232,39 @@ jobs: command: rustc --version; cargo --version; rustup --version; rustup target list --installed - restore_cache: keys: - - cargocache-v2-cw1:1.44.1-{{ checksum "~/project/Cargo.lock" }} + - cargocache-v2-cw2:1.44.1-{{ checksum "~/project/Cargo.lock" }} + - run: + name: Add wasm32 target + command: rustup target add wasm32-unknown-unknown && rustup target list --installed + - run: + name: Build library for native target + command: cargo build --locked + - run: + name: Build library for wasm target + command: cargo wasm --locked + - run: + name: Run unit tests + command: cargo test --locked + # note: there are no schemas to generate + - save_cache: + paths: + - /usr/local/cargo/registry + - target + key: cargocache-v2-cw2:1.44.1-{{ checksum "~/project/Cargo.lock" }} + + package_cw20: + docker: + - image: rust:1.44.1 + working_directory: ~/project/packages/cw20 + steps: + - checkout: + path: ~/project + - run: + name: Version information + command: rustc --version; cargo --version; rustup --version; rustup target list --installed + - restore_cache: + keys: + - cargocache-v2-cw20:1.44.1-{{ checksum "~/project/Cargo.lock" }} - run: name: Add wasm32 target command: rustup target add wasm32-unknown-unknown && rustup target list --installed @@ -260,7 +293,7 @@ jobs: paths: - /usr/local/cargo/registry - target - key: cargocache-v2-cw1:1.44.1-{{ checksum "~/project/Cargo.lock" }} + key: cargocache-v2-cw20:1.44.1-{{ checksum "~/project/Cargo.lock" }} lint: docker: diff --git a/Cargo.lock b/Cargo.lock index b9131a80d..c66ab61cd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -110,6 +110,16 @@ dependencies = [ "snafu", ] +[[package]] +name = "cw2" +version = "0.1.0" +dependencies = [ + "cosmwasm-std", + "cosmwasm-storage", + "schemars", + "serde", +] + [[package]] name = "cw20" version = "0.1.0" diff --git a/packages/cw2/.cargo/config b/packages/cw2/.cargo/config new file mode 100644 index 000000000..e82e5693f --- /dev/null +++ b/packages/cw2/.cargo/config @@ -0,0 +1,3 @@ +[alias] +wasm = "build --release --target wasm32-unknown-unknown" +wasm-debug = "build --target wasm32-unknown-unknown" diff --git a/packages/cw2/Cargo.toml b/packages/cw2/Cargo.toml new file mode 100644 index 000000000..90eca337d --- /dev/null +++ b/packages/cw2/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "cw2" +version = "0.1.0" +authors = ["Ethan Frey "] +edition = "2018" +description = "Definition and types for the CosmWasm-2 interface" +license = "Apache-2.0" + +[dependencies] +cosmwasm-std = { version = "0.10.0" } +cosmwasm-storage = { version = "0.10.0" } +schemars = "0.7" +serde = { version = "1.0.103", default-features = false, features = ["derive"] } \ No newline at end of file diff --git a/packages/cw2/NOTICE b/packages/cw2/NOTICE new file mode 100644 index 000000000..82362cb1e --- /dev/null +++ b/packages/cw2/NOTICE @@ -0,0 +1,14 @@ +CW2: A CosmWasm spec for migration info +Copyright (C) 2020 Confio OÜ + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/packages/cw2/README.md b/packages/cw2/README.md new file mode 100644 index 000000000..4907130ed --- /dev/null +++ b/packages/cw2/README.md @@ -0,0 +1,57 @@ +# CW2 Spec: Contract Info for Migration + +Most of the CW* specs are focused on the *public interfaces* +of the contract. The APIs used for `HandleMsg` or `QueryMsg`. +However, when we wish to migrate from contract A to contract B, +contract B needs to be aware somehow of how the *state was encoded*. + +Generally we use Singletons and Buckets to store the state, but +if I upgrade to a `cw20-with-bonding-curve` contract, it will only +work properly if I am migrating from a `cw20-base` contract. But how +can the new contract know what format the data was stored. + +This is where CW2 comes in. It specifies on special Singleton to +be stored on disk by all contracts on `init`. When the `migrate` +function is called, then the new contract can read that data and +see if this is an expected contract we can migrate from. And also +contain extra version information if we support multiple migrate +paths. + +### Data structures + +**Required** + +All CW2-compliant contracts must store the following data: + +* key: `\x00\x0dcontract_info` (length prefixed "contract_info" using Singleton pattern) +* data: Json-serialized `ContractVersion` + +```rust +pub struct ContractVersion { + /// contract is a globally unique identifier for the contract. + /// it should build off standard namespacing for whichever language it is in, + /// and prefix it with the registry we use. + /// For rust we prefix with `crates.io:`, to give us eg. `crates.io:cw20-base` + pub contract: String, + /// version is any string that this implementation knows. It may be simple counter "1", "2". + /// or semantic version on release tags "v0.6.2", or some custom feature flag list. + /// the only code that needs to understand the version parsing is code that knows how to + /// migrate from the given contract (and is tied to it's implementation somehow) + pub version: String, +} +``` + +Thus, an serialized example may looks like: + +```json +{ + "contract": "crates.io:cw20-base", + "version": "v0.1.0" +} +``` + +### Queries + +Since the state is well defined, we do not need to support any "smart queries". +We do provide a helper to construct a "raw query" to read the ContractInfo +of any CW2-compliant contract. diff --git a/packages/cw2/src/lib.rs b/packages/cw2/src/lib.rs new file mode 100644 index 000000000..b7168fe95 --- /dev/null +++ b/packages/cw2/src/lib.rs @@ -0,0 +1,71 @@ +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use cosmwasm_std::{ + HumanAddr, Querier, QueryRequest, ReadonlyStorage, StdResult, Storage, WasmQuery, +}; +use cosmwasm_storage::{to_length_prefixed, ReadonlySingleton, Singleton}; + +pub const PREFIX_INFO: &[u8] = b"contract_info"; + +#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] +pub struct ContractVersion { + /// contract is the crate name of the implementing contract, eg. `crate:cw20-base` + /// we will use other prefixes for other languages, and their standard global namespacing + pub contract: String, + /// version is any string that this implementation knows. It may be simple counter "1", "2". + /// or semantic version on release tags "v0.6.2", or some custom feature flag list. + /// the only code that needs to understand the version parsing is code that knows how to + /// migrate from the given contract (and is tied to it's implementation somehow) + pub version: String, +} + +/// get_contract_info can be use in migrate to read the previous version of this contract +pub fn get_contract_info(storage: &S) -> StdResult { + ReadonlySingleton::new(storage, PREFIX_INFO).load() +} + +/// set_contract_info should be used in init to store the original version, and after a successful +/// migrate to update it +pub fn set_contract_info(storage: &mut S, info: &ContractVersion) -> StdResult<()> { + Singleton::new(storage, PREFIX_INFO).save(info) +} + +/// This will make a raw_query to another contract to determine the current version it +/// claims to be. This should not be trusted, but could be used as a quick filter +/// if the other contract exists and claims to be a cw20-base contract for example. +/// (Note: you usually want to require *interfaces* not *implementations* of the +/// contracts you compose with, so be careful of overuse) +pub fn query_contract_info>( + querier: &Q, + contract_addr: T, +) -> StdResult { + let req = QueryRequest::Wasm(WasmQuery::Raw { + contract_addr: contract_addr.into(), + key: to_length_prefixed(PREFIX_INFO).into(), + }); + querier.query(&req) +} + +#[cfg(test)] +mod tests { + use super::*; + use cosmwasm_std::testing::MockStorage; + + #[test] + fn get_and_set_work() { + let mut store = MockStorage::new(); + + // error if not set + assert!(get_contract_info(&store).is_err()); + + // set and get + let info = ContractVersion { + contract: "crate:cw20-base".to_string(), + version: "v0.1.0".to_string(), + }; + set_contract_info(&mut store, &info).unwrap(); + let loaded = get_contract_info(&store).unwrap(); + assert_eq!(info, loaded); + } +}