├── .github ├── actions │ └── install-rust │ │ ├── README.md │ │ ├── action.yml │ │ └── main.js └── workflows │ └── Static.yml ├── .gitignore ├── Cargo.toml ├── LICENSE ├── Makefile ├── Readme.md ├── Releases.md ├── packages ├── crypto │ ├── Cargo.toml │ ├── Readme.md │ └── src │ │ ├── hash.rs │ │ ├── hkdf.rs │ │ ├── lib.rs │ │ ├── rng.rs │ │ └── secp256k1.rs ├── incubator │ ├── Cargo.toml │ ├── Readme.md │ └── src │ │ ├── generational_store.rs │ │ ├── lib.rs │ │ └── maxheap.rs ├── notification │ ├── Cargo.toml │ ├── Readme.md │ └── src │ │ ├── cbor.rs │ │ ├── cipher.rs │ │ ├── funcs.rs │ │ ├── lib.rs │ │ └── structs.rs ├── permit │ ├── Cargo.toml │ ├── Readme.md │ └── src │ │ ├── funcs.rs │ │ ├── lib.rs │ │ ├── state.rs │ │ └── structs.rs ├── serialization │ ├── Cargo.toml │ ├── Readme.md │ └── src │ │ ├── base64.rs │ │ ├── bincode2.rs │ │ ├── json.rs │ │ └── lib.rs ├── snip20 │ ├── Cargo.toml │ ├── Readme.md │ └── src │ │ ├── batch.rs │ │ ├── handle.rs │ │ ├── lib.rs │ │ └── query.rs ├── snip721 │ ├── Cargo.toml │ ├── Readme.md │ └── src │ │ ├── expiration.rs │ │ ├── handle.rs │ │ ├── lib.rs │ │ ├── metadata.rs │ │ └── query.rs ├── storage │ ├── Cargo.toml │ ├── Readme.md │ └── src │ │ ├── append_store.rs │ │ ├── deque_store.rs │ │ ├── item.rs │ │ ├── keymap.rs │ │ ├── keyset.rs │ │ ├── lib.rs │ │ └── secure_item.rs ├── utils │ ├── Cargo.toml │ ├── Readme.md │ └── src │ │ ├── calls.rs │ │ ├── feature_toggle.rs │ │ ├── lib.rs │ │ ├── padding.rs │ │ └── types.rs └── viewing_key │ ├── Cargo.toml │ ├── Readme.md │ └── src │ └── lib.rs └── src └── lib.rs /.github/actions/install-rust/README.md: -------------------------------------------------------------------------------- 1 | # install-rust 2 | 3 | A small github action to install `rustup` and a Rust toolchain. This is 4 | generally expressed inline, but it was repeated enough in this repository it 5 | seemed worthwhile to extract. 6 | 7 | Some gotchas: 8 | 9 | * Can't `--self-update` on Windows due to permission errors (a bug in Github 10 | Actions) 11 | * `rustup` isn't installed on macOS (a bug in Github Actions) 12 | 13 | When the above are fixed we should delete this action and just use this inline: 14 | 15 | ```yml 16 | - run: rustup update $toolchain && rustup default $toolchain 17 | shell: bash 18 | ``` -------------------------------------------------------------------------------- /.github/actions/install-rust/action.yml: -------------------------------------------------------------------------------- 1 | name: 'Install Rust toolchain' 2 | description: 'Install both `rustup` and a Rust toolchain' 3 | 4 | inputs: 5 | toolchain: 6 | description: 'Default toolchain to install' 7 | required: false 8 | default: 'stable' 9 | 10 | runs: 11 | using: node12 12 | main: 'main.js' -------------------------------------------------------------------------------- /.github/actions/install-rust/main.js: -------------------------------------------------------------------------------- 1 | const child_process = require('child_process'); 2 | const toolchain = process.env.INPUT_TOOLCHAIN; 3 | const fs = require('fs'); 4 | 5 | function set_env(name, val) { 6 | fs.appendFileSync(process.env['GITHUB_ENV'], `${name}=${val}\n`) 7 | } 8 | 9 | // Needed for now to get 1.24.2 which fixes a bug in 1.24.1 that causes issues 10 | // on Windows. 11 | if (process.platform === 'win32') { 12 | child_process.execFileSync('rustup', ['self', 'update']); 13 | } 14 | 15 | child_process.execFileSync('rustup', ['set', 'profile', 'minimal']); 16 | child_process.execFileSync('rustup', ['update', toolchain, '--no-self-update']); 17 | child_process.execFileSync('rustup', ['default', toolchain]); 18 | 19 | // Deny warnings on CI to keep our code warning-free as it lands in-tree. Don't 20 | // do this on nightly though since there's a fair amount of warning churn there. 21 | // RUSTIX: Disable this so that it doesn't overwrite RUSTFLAGS for setting 22 | // "--cfg rustix_use_libc". We re-add it manually in the workflow. 23 | //if (!toolchain.startsWith('nightly')) { 24 | // set_env("RUSTFLAGS", "-D warnings"); 25 | //} 26 | 27 | // Save disk space by avoiding incremental compilation, and also we don't use 28 | // any caching so incremental wouldn't help anyway. 29 | set_env("CARGO_INCREMENTAL", "0"); 30 | 31 | // Turn down debuginfo from 2 to 1 to help save disk space 32 | set_env("CARGO_PROFILE_DEV_DEBUG", "1"); 33 | set_env("CARGO_PROFILE_TEST_DEBUG", "1"); 34 | 35 | if (process.platform === 'darwin') { 36 | set_env("CARGO_PROFILE_DEV_SPLIT_DEBUGINFO", "unpacked"); 37 | set_env("CARGO_PROFILE_TEST_SPLIT_DEBUGINFO", "unpacked"); 38 | } -------------------------------------------------------------------------------- /.github/workflows/Static.yml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | 3 | name: Static Checks 4 | 5 | jobs: 6 | tests: 7 | name: ${{ matrix.make.name }} (${{ matrix.os }}) 8 | runs-on: ${{ matrix.os }} 9 | strategy: 10 | fail-fast: false 11 | matrix: 12 | os: [ubuntu-latest] 13 | rust: [stable] 14 | make: 15 | - name: Clippy 16 | task: "cargo clippy --all --all-features -- -D warnings" 17 | - name: Unit tests 18 | task: "cargo test --all --all-features" 19 | include: 20 | - os: ubuntu-latest 21 | sccache-path: /home/runner/.cache/sccache 22 | env: 23 | RUST_BACKTRACE: full 24 | RUSTC_WRAPPER: sccache 25 | RUSTV: ${{ matrix.rust }} 26 | SCCACHE_CACHE_SIZE: 2G 27 | SCCACHE_DIR: ${{ matrix.sccache-path }} 28 | # SCCACHE_RECACHE: 1 # Uncomment this to clear cache, then comment it back out 29 | steps: 30 | - uses: actions/checkout@v2 31 | - name: Install sccache (ubuntu-latest) 32 | if: matrix.os == 'ubuntu-latest' 33 | env: 34 | LINK: https://github.com/mozilla/sccache/releases/download 35 | SCCACHE_VERSION: v0.2.15 36 | run: | 37 | SCCACHE_FILE=sccache-$SCCACHE_VERSION-x86_64-unknown-linux-musl 38 | mkdir -p $HOME/.local/bin 39 | curl -L "$LINK/$SCCACHE_VERSION/$SCCACHE_FILE.tar.gz" | tar xz 40 | mv -f $SCCACHE_FILE/sccache $HOME/.local/bin/sccache 41 | chmod +x $HOME/.local/bin/sccache 42 | echo "$HOME/.local/bin" >> $GITHUB_PATH 43 | - uses: ./.github/actions/install-rust 44 | with: 45 | toolchain: ${{ matrix.rust }} 46 | - name: Add wasm toolchain 47 | run: | 48 | rustup target add wasm32-unknown-unknown 49 | - name: Install Clippy 50 | if: matrix.make.name == 'Clippy' 51 | run: | 52 | rustup component add clippy 53 | - name: Cache cargo registry 54 | uses: actions/cache@v3 55 | continue-on-error: false 56 | with: 57 | path: | 58 | ~/.cargo/registry 59 | ~/.cargo/git 60 | key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} 61 | restore-keys: | 62 | ${{ runner.os }}-cargo- 63 | - name: Save sccache 64 | uses: actions/cache@v3 65 | continue-on-error: false 66 | with: 67 | path: ${{ matrix.sccache-path }} 68 | key: ${{ runner.os }}-sccache-${{ hashFiles('**/Cargo.lock') }} 69 | restore-keys: | 70 | ${{ runner.os }}-sccache- 71 | - name: Start sccache server 72 | run: sccache --start-server 73 | - name: ${{ matrix.make.name }} 74 | run: ${{ matrix.make.task }} 75 | - name: Print sccache stats 76 | run: sccache --show-stats 77 | - name: Stop sccache server 78 | run: sccache --stop-server || true 79 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | Cargo.lock 3 | 4 | .idea 5 | .vscode -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "secret-toolkit" 3 | version = "0.10.3" 4 | edition = "2021" 5 | authors = ["SCRT Labs "] 6 | license-file = "LICENSE" 7 | repository = "https://github.com/scrtlabs/secret-toolkit" 8 | readme = "Readme.md" 9 | description = "Various tools for writing Secret Contracts" 10 | categories = ["cryptography::cryptocurrencies", "wasm"] 11 | keywords = ["secret-network", "secret-contracts", "secret-toolkit"] 12 | 13 | [package.metadata.docs.rs] 14 | all-features = true 15 | 16 | [features] 17 | default = ["serialization", "snip20", "snip721", "storage", "utils"] 18 | 19 | crypto = [ 20 | "secret-toolkit-crypto", 21 | ] # Not in default features because this is slow to compile 22 | incubator = [ 23 | "secret-toolkit-incubator", 24 | "serialization", 25 | ] # Should never be in default features! 26 | permit = [ 27 | "secret-toolkit-permit", 28 | "crypto", 29 | "utils", 30 | ] # Not in default features because it requires "crypto" 31 | serialization = ["secret-toolkit-serialization"] 32 | snip20 = ["secret-toolkit-snip20", "utils"] 33 | snip721 = ["secret-toolkit-snip721", "utils"] 34 | storage = ["secret-toolkit-storage", "serialization"] 35 | utils = ["secret-toolkit-utils"] 36 | viewing-key = ["secret-toolkit-viewing-key"] 37 | notification = ["secret-toolkit-notification"] 38 | 39 | [dependencies] 40 | secret-toolkit-crypto = { version = "0.10.3", path = "packages/crypto", optional = true } 41 | secret-toolkit-incubator = { version = "0.10.3", path = "packages/incubator", optional = true } 42 | secret-toolkit-permit = { version = "0.10.3", path = "packages/permit", optional = true } 43 | secret-toolkit-serialization = { version = "0.10.3", path = "packages/serialization", optional = true } 44 | secret-toolkit-snip20 = { version = "0.10.3", path = "packages/snip20", optional = true } 45 | secret-toolkit-snip721 = { version = "0.10.3", path = "packages/snip721", optional = true } 46 | secret-toolkit-storage = { version = "0.10.3", path = "packages/storage", optional = true } 47 | secret-toolkit-utils = { version = "0.10.3", path = "packages/utils", optional = true } 48 | secret-toolkit-viewing-key = { version = "0.10.3", path = "packages/viewing_key", optional = true } 49 | secret-toolkit-notification = { version = "0.10.3", path = "packages/notification", optional = true } 50 | 51 | [workspace] 52 | members = ["packages/*"] 53 | # Since `secret-toolkit` depends on all the other packages, this should make `cargo-check` a bit quicker 54 | # as it won't have to check all the other packages twice. 55 | #default-members = ["packages/toolkit"] 56 | # exclude = ["packages/crypto"] 57 | 58 | [workspace.dependencies] 59 | schemars = { version = "0.8.11" } 60 | serde = { version = "1.0" } 61 | cosmwasm-std = { package = "secret-cosmwasm-std", version = "1.1.11", features = [ 62 | "random", 63 | ] } 64 | cosmwasm-storage = { package = "secret-cosmwasm-storage", version = "1.1.11" } 65 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: check 2 | check: 3 | cargo check --workspace 4 | 5 | .PHONY: clippy 6 | clippy: 7 | cargo clippy --workspace 8 | 9 | .PHONY: test 10 | test: 11 | cargo test --workspace 12 | 13 | .PHONY: publish 14 | publish: 15 | cargo publish -p secret-toolkit-crypto 16 | cargo publish -p secret-toolkit-serialization 17 | cargo publish -p secret-toolkit-incubator 18 | cargo publish -p secret-toolkit-permit 19 | cargo publish -p secret-toolkit-utils 20 | cargo publish -p secret-toolkit-snip20 21 | cargo publish -p secret-toolkit-snip721 22 | cargo publish -p secret-toolkit-storage 23 | cargo publish -p secret-toolkit-viewing-key 24 | cargo publish -p secret-toolkit-notification 25 | cargo publish 26 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # Secret Contract Development Toolkit 2 | 3 | This Rust package is a collection of sub-packages that contain common tools used in development of 4 | [Secret Contracts](https://docs.scrt.network/dev/secret-contracts.html) running on the 5 | [Secret Network](https://scrt.network/). 6 | 7 | The main package in this repository is `secret-toolkit` under `packages/toolkit`, which is 8 | a wrapper around the other packages. For example `secret-toolkit-storage` is exported under 9 | `secret_toolkit::storage`. If you only need some of the tools from the toolkit, you may get 10 | better compile times by depending on the different components directly. 11 | 12 | Each of the subpackages is imported with a feature flag, and most subpackages are included 13 | in the default flags. The exceptions to this are: 14 | * `"crypto"` - has a deep dependency tree and increases compilation times significantly 15 | * `"permit"` - depends on `"crypto"` and imports it automatically 16 | * `"incubator"` - includes experimental functionality. Minor version releases may cause 17 | breaking changes in this subpackage. 18 | 19 | While the packages in this repository are designed with Secret Network's runtime in mind, some 20 | of them may work well with the vanilla [CosmWasm](https://cosmwasm.com/) libraries and runtimes 21 | as well, or only require minimal modifications to be compatible with them. 22 | 23 | ## License 24 | 25 | The license file in the top directory of this repository applies to all packages it contains. 26 | -------------------------------------------------------------------------------- /Releases.md: -------------------------------------------------------------------------------- 1 | # Release notes for the Secret Toolkit 2 | 3 | ## Unreleased 4 | 5 | ## v0.10.3 6 | 7 | ### Bug fixes 8 | 9 | - fix out of bounds bug at get_at function in append_store 10 | 11 | ## v0.10.2 12 | 13 | ### Features 14 | 15 | - Added a new `notification` package with data structures and functions to help create SNIP-52 private push notifications compliant secret contracts ([#99](https://github.com/scrtlabs/secret-toolkit/pull/99)). 16 | 17 | ## v0.10.1 18 | 19 | ### Bug fixes 20 | 21 | - Fixed `Wasm contract requires unsupported import: "env.rustsecp256k1_v0_8_1_context_preallocated_size` error by freezing `cc` version to `1.1.10`. 22 | 23 | ## v0.10.0 24 | 25 | ### Features 26 | 27 | - Bumped `cosmwasm-std` version to `v1.1.11` ([#93]). 28 | 29 | ### Breaking 30 | 31 | - Added optional `admin` field to `utils::InitCallback::to_cosmos_msg` ([#93]). 32 | 33 | ### Bug fixes 34 | 35 | - Only padding encrypted attributes in `utils::pad_handle_result` ([#92]). 36 | - Support `backtraces` feature for `KeyMap` and `KeySet` ([#90]). 37 | 38 | [#90]: https://github.com/scrtlabs/secret-toolkit/pull/90 39 | [#92]: https://github.com/scrtlabs/secret-toolkit/pull/92 40 | [#93]: https://github.com/scrtlabs/secret-toolkit/pull/93 41 | 42 | ## v0.9.0 43 | 44 | ### Features 45 | 46 | - Replace `cosmwasm-std` with `secret-cosmwasm-std` in prep for crates.io packages ([#87](https://github.com/scrtlabs/secret-toolkit/pull/87)). 47 | - Add `RngCore` & `CryptoRng` trait to `Prng` ([#87](https://github.com/scrtlabs/secret-toolkit/pull/87)). 48 | - Added `from_env` function for `ContractPrng` that consumes `env.block.random` ([#87](https://github.com/scrtlabs/secret-toolkit/pull/87)). 49 | 50 | ### Breaking 51 | 52 | - Renamed `Prng` as `ContractPrng` ([#87](https://github.com/scrtlabs/secret-toolkit/pull/87)). 53 | 54 | ## v0.8.2 55 | 56 | ### Bug fixes 57 | 58 | - Fixed a remove bug in `Keymap` and `Keyset` ([#86](https://github.com/scrtlabs/secret-toolkit/pull/86)). 59 | 60 | ## v0.8.1 61 | 62 | ### Bug fixes 63 | 64 | - Fixed a bug in `Keymap` and `Keyset` ([#84](https://github.com/scrtlabs/secret-toolkit/pull/84)). 65 | 66 | ### Features 67 | 68 | - SecureItem - storage access pattern obfuscating Item ([#82](https://github.com/scrtlabs/secret-toolkit/pull/82)). 69 | - Change the internal `rng` field of the `Prng` struct to be public ([#81](https://github.com/scrtlabs/secret-toolkit/pull/81)), 70 | 71 | ## v0.8.0 72 | 73 | This release upgrades all `secret-toolkit` packages to be compatible with Cosmwasm v1.1. 74 | The APIs remains the same, but it is necessary to upgrade the contract's `cosmwasm` dependencies to `v1.1.0` 75 | 76 | ### Breaking 77 | 78 | - Since `cosmwasm v1.1` had some breaking changes to it's dependencies, this version will not work with `cosmwasm v1`. It is necessary to upgrade to `cosmwasm v1.1` in order to use this release and vice verca. However, neither `cosmwasm v1.1` or this version did not have breaking changes to the APIs. 79 | 80 | ## v0.7.0 81 | 82 | - This release changes the internal toolkit package to be part of the workspace - this fixes default-features flags in some of the crates. In addition, crates used by the toolkit have been bumped, and the edition of the toolkit crates has been bumped to 2021. 83 | 84 | - Added the `Keyset` storage object (A hashset like storage object). 85 | - Allowed further customisation of Keymap and Keyset with new constructor structs called `KeymapBuilder` and `KeysetBuilder` which allow the user to disable the iterator feature (saving gas) or adjust the internal indexes' page size so that the user may determine how many objects are to be stored/loaded together in the iterator. 86 | - `::new_with_page_size(namespace, page_size)` method was added to `AppendStore` and `DequeStore` so that the user may adjust the internal indexes' page size which determine how many objects are to be stored/loaded together in the iterator. 87 | - Minor performance upgrades to `Keymap`, `AppendStore`, and `DequeStore`. 88 | 89 | ### Breaking 90 | 91 | - Older rust compilers ( < 1.50 ) may not work due to upgraded dependencies 92 | 93 | ## v0.6.0 94 | 95 | This release upgrades all `secret-toolkit` packages to be compatible with Cosmwasm v1.0 (Secret Network v1.4). 96 | The APIs remains the same, but it is necessary to upgrade the contract's `cosmwasm` dependencies to `v1.0.0`. 97 | 98 | ### Breaking 99 | 100 | - This version will not work with `cosmwasm v0.10`. It is necessary to upgrade to `cosmwasm v1` in order to use this release. 101 | 102 | ## v0.5.0 103 | 104 | This release includes some minor fixed to the storage package which required some breaking changes. 105 | We are releasing these breaking changes because we reached the conclusion that the current interfaces 106 | are prone to bugs, or inefficient. Unless you are using these specific interfaces, you should be able to upgrade from 0.4 without issues. 107 | 108 | ### Breaking 109 | 110 | - Removed the implementations of Clone for storage types which are not useful and may cause data corruption if used incorrectly. 111 | - Changed `Keymap::insert` to take the item by reference rather than by value. This should reduce the cost of calling that function by avoiding cloning. 112 | 113 | ### Features 114 | 115 | - Changed the implementation of the `add_prefix` methods in the storage package to use length prefixing, which should help avoid namespace collisions. 116 | 117 | ## secret-toolkit-storage v0.4.2 118 | 119 | - BUGFIX: implementation of `.clone` method fixed 120 | - Added `.add_suffix` and `.clone` methods to `secret-toolkit::storage::Item` 121 | - Minor performance updates to `secret-toolkit::storage::Keymap` 122 | 123 | ## secret-toolkit-storage v0.4.1 124 | 125 | - BUGFIX: `Item::is_empty` was returning the opposite value from what you'd expect. 126 | 127 | ## v0.4.0 128 | 129 | This release mostly includes the work of @srdtrk in #53. Thanks Srdtrk! 130 | 131 | It revamps the `secret-toolkit-storage` package to make it more similar to `cw-storage-plus` and much easier 132 | to use. It also removes the `Cashmap` type from the incubator in favor of `KeyMap` in `secret-toolkit-storage`. 133 | 134 | This is a summary of the changes and additions in this release: 135 | 136 | - Minimum Rust version is bumped to the latest v1.63. This is because we want to use `Mutex::new` in a `const fn`. 137 | - No more distinction between `Readonly*` and `*Mut` types. Instead, methods take references or mutable references to the storage every time. 138 | - Usage of `PrefixedStore` is made mostly unnecessary. 139 | - Storage type's constructors are const functions, which means they can be initialized as global static variables. 140 | - Added `secret-toolkit::storage::Item` which is similar to `Item` from `cw-storage-plus` or `TypedStore` from `cosmwasm_storage` v0.10. 141 | - Added `secret-toolkit::storage::KeyMap` which is similar to `Cashmap`. 142 | - `Cashmap` is completely removed. 143 | 144 | A full guide to using the new `storage` types can be found 145 | [in the package's readme file](https://github.com/srdtrk/secret-toolkit/blob/3725530aebe149d14f7f3f1662844340eb27e015/packages/storage/Readme.md). 146 | 147 | ## secret-toolkit-incubator v0.3.1 148 | 149 | - Fixed compilation issue with Rust v1.61 (#46, #48) 150 | - Removed Siphasher dependency (#46, #48) 151 | 152 | ## secret-toolkit-utils v0.3.1 153 | 154 | ### Security 155 | 156 | - BUGFIX: `secret-toolkit::utils::FeatureToggle::handle_pause` had an inverse authorization check: only non-pausers 157 | could pause features. 158 | 159 | ## secret-toolkit-permit v0.3.1 160 | 161 | - Removed the `ecc-secp256k1` feature from `secret-toolkit-crypto` dependency of `secret-toolkit-permit`. 162 | - This tiny change significantly reduces the size of binaries that only use the permit feature. 163 | 164 | ## v0.3.0 165 | 166 | - Added `clear` method to `AppendStore` and `DequeStore` to quickly reset the collections (#34) 167 | - docs.rs documentation now includes all sub-crates. 168 | - BUGFIX: `secret-toolkit::snip721::Metadata` was severely out of date with the SNIP-721 specification, and not useful. 169 | It is now compatible with deployed SNIP-721 contracts. 170 | 171 | - Added `types` module under the `util` package, to standardize often used types. 172 | - Added `secret-toolkit::viewing_key`, which can be imported by enabling the `viewing-key` feature. 173 | - Added `secret-toolkit::permit::PubKey::canonical_address()`. 174 | - Types in `secret-toolkit::permit::Permit` are now generic over the type of permissions they accept. 175 | - Added the `maxheap` type to the incubator. 176 | - Added `secret-toolkit::utils::feature_toggle` which allow managing feature flags in your contract. 177 | 178 | ### Breaking 179 | 180 | - `secret-toolkit::permit::validate()` Now supports validating any type of Cosmos address. 181 | Interface changes: Now takes a reference to the current token address instead 182 | of taking it by value and an optional hrp string. 183 | In addition, it returns a String and not HumanAddr. 184 | 185 | - Renamed `secret-toolkit::permit::Permission` to `secret-toolkit::permit::TokenPermission`. 186 | - `secret-toolkit-crypto` now has features `["hash", "rng" and "ecc-secp256k1"]` which are all off by default - enable those you need. 187 | - `secret-toolkit-crypto::secp256k1::PublicKey::parse` now returns `StdResult`. 188 | - Changes to `secret-toolkit::crypto::secp256k1::PrivateKey::sign`: 189 | - The `data` argument is now any slice of bytes, and not the hash of a slice of data. 190 | - the `Api` from `deps.api` is now required as the second argument as we now use the precompiled implementation. 191 | - Changes to `secret-toolkit::crypto::secp256k1::PublicKey::verify`: 192 | - the `Api` from `deps.api` is now required as the third argument as we now use the precompiled implementation. 193 | - `secret-toolkit-incubator` now has features `["cashmap", "generational-store"]` which are all off by default. 194 | 195 | ## v0.2.0 196 | 197 | This release includes a ton of new features, and a few breaking changes in various interfaces. 198 | This version is also the first released to [crates.io](https://crates.io)! 199 | 200 | - Change: when a query fails because of a bad viewing key, this now correctly fails with `StdError::Unauthorized` 201 | - Added support for some missing SNIP-20 functionality, such as `CreateViewingKey` 202 | - Added support for SNIP-21 queries (memos and improved history) which broke some interfaces 203 | - Added support for SNIP-22 messages (batch operations) 204 | - Added support for SNIP-23 messages (improved Send operations) which broke some interfaces 205 | - Added support for SNIP-24 permits 206 | - Added `Base64Of`, `Base64JsonOf`, and `Base64Bincode2Of`, 207 | which are wrappers that automatically deserializes base64 strings to `T`. 208 | It can be used in message types' fields instead of `Binary` when the contents of the string 209 | should have more specific contents. 210 | 211 | - Added `storage::DequeStore` - Similar to `AppendStore` but allows pushing and popping on both ends 212 | - Added the `secret-toolkit::incubator` package intended for experimental features. It contains: 213 | - `CashMap` - A hashmap like storage abstraction 214 | - `GenerationalIndex` - A generational index storage abstraction 215 | - The various subpackages can now be selected using feature flags. The default flags are `["serialization", "snip20", "snip721", "storage", "utils"]` 216 | while `["crypto", "permit", "incubator"]` are left disabled by default. 217 | 218 | ## v0.1.1 219 | 220 | - Removed unused dev-dependency that was slowing down test compilation times. 221 | 222 | ## v0.1.0 223 | 224 | This is the first release of `secret-toolkit`. It supports: 225 | 226 | - `secret-toolkit::snip20` - Helper types and functions for interaction with 227 | SNIP-20 contracts. 228 | - `secret-toolkit::snip721` - Helper types and functions for interaction with 229 | SNIP-721 contracts. 230 | - `secret-toolkit::crypto` - Wrappers for known-to-work crypto primitives from 231 | ecosystem libraries. We include implementations for Sha256, Secp256k1 keys, 232 | and ChaChaRng. 233 | - `secret-toolkit::storage` - Types implementing useful storage managements 234 | techniques: `AppendStore` and `TypedStore`, using `bincode2` by default. 235 | - `secret-toolkit::serialization` - marker types for overriding the storage 236 | format used by types in `secret-toolkit::storage`. `Json` and `Bincode2`. 237 | 238 | - `secret-toolkit::utils` - General utilities for writing contract code. 239 | - `padding` - tools for padding queries and responses. 240 | - `calls` - Tools for marking types as messages in queries and callbacks 241 | to other contracts. 242 | -------------------------------------------------------------------------------- /packages/crypto/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "secret-toolkit-crypto" 3 | version = "0.10.3" 4 | edition = "2021" 5 | authors = ["SCRT Labs "] 6 | license-file = "../../LICENSE" 7 | repository = "https://github.com/scrtlabs/secret-toolkit" 8 | readme = "Readme.md" 9 | description = "Cryptographic tools for writing Secret Contracts" 10 | categories = ["cryptography::cryptocurrencies", "wasm"] 11 | keywords = ["secret-network", "secret-contracts", "secret-toolkit"] 12 | 13 | [package.metadata.docs.rs] 14 | all-features = true 15 | 16 | [features] 17 | default = ["hash", "ecc-secp256k1", "rand"] 18 | hash = ["sha2"] 19 | ecc-secp256k1 = ["secp256k1"] 20 | rand = ["hash", "rand_chacha", "rand_core"] 21 | hkdf = ["sha2"] 22 | 23 | [dependencies] 24 | rand_core = { version = "0.6.4", default-features = false, optional = true } 25 | rand_chacha = { version = "0.3.1", default-features = false, optional = true } 26 | sha2 = { version = "0.10.6", default-features = false, optional = true } 27 | secp256k1 = { version = "0.27.0", default-features = false, features = [ 28 | "alloc", 29 | ], optional = true } 30 | hkdf = "0.12.3" 31 | cosmwasm-std = { workspace = true } 32 | cc = { version = "=1.1.10" } 33 | 34 | [dev-dependencies] 35 | secp256k1 = { version = "0.27.0", default-features = false, features = [ 36 | "alloc", 37 | "rand-std", 38 | ] } 39 | base64 = "0.21.0" 40 | -------------------------------------------------------------------------------- /packages/crypto/Readme.md: -------------------------------------------------------------------------------- 1 | # Secret Contract Development Toolkit - Crypto Tools 2 | 3 | ⚠️ This package is a sub-package of the `secret-toolkit` package. Please see its crate page for more context. 4 | 5 | This crate contains common cryptography tools used in the development of Secret Contracts 6 | running on the Secret Network. 7 | 8 | Note: It has a deep dependency tree and increases compilation times significantly. 9 | 10 | Add the following to your `cargo.toml` file: 11 | 12 | ```toml 13 | [dependencies] 14 | secret-toolkit = { version = "0.10.3", features = ["crypto"] } 15 | secret-toolkit-crypto = { version = "0.10.3", features = ["hash", "rand", "ecc-secp256k1"] } 16 | ``` 17 | 18 | ## Example usage 19 | 20 | ```rust 21 | # extern crate secret_toolkit_crypto; 22 | 23 | # use secret_toolkit_crypto::{sha_256, ContractPrng, secp256k1::{PrivateKey, PublicKey, Signature}}; 24 | # use base64; 25 | # use cosmwasm_std::{StdError, testing::mock_dependencies}; 26 | 27 | # fn main() -> Result<(), StdError> { 28 | let deps = mock_dependencies(); 29 | let entropy: String = "secret".to_owned(); 30 | let prng_seed: Vec = sha_256(base64::encode(&entropy.clone()).as_bytes()).to_vec(); 31 | 32 | let mut rng = ContractPrng::new(&prng_seed, entropy.as_bytes()); 33 | 34 | let private_key: PrivateKey = PrivateKey::parse(&rng.rand_bytes())?; 35 | let public_key: PublicKey = private_key.pubkey(); 36 | 37 | let message: &[u8] = b"message"; 38 | let signature: Signature = private_key.sign(message, deps.api); 39 | # Ok(()) 40 | # } 41 | ``` 42 | 43 | ### Cargo Features 44 | 45 | - `["hash"]` - Provides an easy-to-use `sha256` function. Uses [sha2](https://crates.io/crates/sha2). 46 | - `["rand"]` - Used to generate pseudo-random numbers. Uses [rand_chacha] and [rand_core]. 47 | - `["ecc-secp256k1"]` - Contains types and methods for working with secp256k1 keys and signatures, 48 | as well as standard constants for key sizes. Uses [secp256k1](https://crates.io/crates/secp256k1). 49 | -------------------------------------------------------------------------------- /packages/crypto/src/hash.rs: -------------------------------------------------------------------------------- 1 | use sha2::{Digest, Sha256}; 2 | 3 | pub const SHA256_HASH_SIZE: usize = 32; 4 | 5 | pub fn sha_256(data: &[u8]) -> [u8; SHA256_HASH_SIZE] { 6 | let mut hasher = Sha256::new(); 7 | hasher.update(data); 8 | let hash = hasher.finalize(); 9 | 10 | let mut result = [0u8; 32]; 11 | result.copy_from_slice(hash.as_slice()); 12 | result 13 | } 14 | 15 | #[cfg(test)] 16 | mod tests { 17 | use super::*; 18 | 19 | #[test] 20 | fn test_sha_256() { 21 | let r = sha_256(b"test"); 22 | let r_expected: [u8; SHA256_HASH_SIZE] = [ 23 | 159, 134, 208, 129, 136, 76, 125, 101, 154, 47, 234, 160, 197, 90, 208, 21, 163, 191, 24 | 79, 27, 43, 11, 130, 44, 209, 93, 108, 21, 176, 240, 10, 8, 25 | ]; 26 | assert_eq!(r, r_expected); 27 | 28 | let r = sha_256(b"random_string_123"); 29 | let r_expected: [u8; SHA256_HASH_SIZE] = [ 30 | 167, 75, 46, 161, 27, 233, 254, 146, 245, 218, 2, 19, 171, 56, 78, 166, 42, 211, 88, 7, 31 | 205, 191, 2, 6, 226, 158, 43, 144, 8, 149, 170, 164, 32 | ]; 33 | assert_eq!(r, r_expected); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /packages/crypto/src/hkdf.rs: -------------------------------------------------------------------------------- 1 | use cosmwasm_std::{StdError, StdResult}; 2 | use hkdf::{hmac::Hmac, Hkdf}; 3 | use sha2::{Sha256, Sha512}; 4 | 5 | // Create alias for HMAC-SHA256 6 | pub type HmacSha256 = Hmac; 7 | 8 | pub fn hkdf_sha_256( 9 | salt: &Option>, 10 | ikm: &[u8], 11 | info: &[u8], 12 | length: usize, 13 | ) -> StdResult> { 14 | let hk: Hkdf = Hkdf::::new(salt.as_deref(), ikm); 15 | let mut zero_bytes = vec![0u8; length]; 16 | let okm = zero_bytes.as_mut_slice(); 17 | match hk.expand(info, okm) { 18 | Ok(_) => Ok(okm.to_vec()), 19 | Err(e) => Err(StdError::generic_err(format!("{:?}", e))), 20 | } 21 | } 22 | 23 | pub fn hkdf_sha_512( 24 | salt: &Option>, 25 | ikm: &[u8], 26 | info: &[u8], 27 | length: usize, 28 | ) -> StdResult> { 29 | let hk: Hkdf = Hkdf::::new(salt.as_deref(), ikm); 30 | let mut zero_bytes = vec![0u8; length]; 31 | let okm = zero_bytes.as_mut_slice(); 32 | match hk.expand(info, okm) { 33 | Ok(_) => Ok(okm.to_vec()), 34 | Err(e) => Err(StdError::generic_err(format!("{:?}", e))), 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /packages/crypto/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![doc = include_str!("../Readme.md")] 2 | 3 | #[cfg(feature = "hash")] 4 | mod hash; 5 | #[cfg(feature = "rand")] 6 | mod rng; 7 | #[cfg(feature = "ecc-secp256k1")] 8 | pub mod secp256k1; 9 | 10 | #[cfg(feature = "hash")] 11 | pub use hash::{sha_256, SHA256_HASH_SIZE}; 12 | 13 | #[cfg(feature = "rand")] 14 | pub use rng::ContractPrng; 15 | 16 | #[cfg(feature = "hkdf")] 17 | pub mod hkdf; 18 | #[cfg(feature = "hkdf")] 19 | pub use crate::hkdf::*; 20 | -------------------------------------------------------------------------------- /packages/crypto/src/rng.rs: -------------------------------------------------------------------------------- 1 | use rand_chacha::ChaChaRng; 2 | use rand_core::{CryptoRng, RngCore, SeedableRng}; 3 | use sha2::{Digest, Sha256}; 4 | 5 | use cosmwasm_std::Env; 6 | 7 | pub struct ContractPrng { 8 | pub rng: ChaChaRng, 9 | } 10 | 11 | impl ContractPrng { 12 | pub fn from_env(env: &Env) -> Self { 13 | let seed = env.block.random.as_ref().unwrap(); 14 | 15 | Self::new(seed.as_slice(), &[]) 16 | } 17 | 18 | pub fn new(seed: &[u8], entropy: &[u8]) -> Self { 19 | let mut hasher = Sha256::new(); 20 | 21 | // write input message 22 | hasher.update(seed); 23 | hasher.update(entropy); 24 | let hash = hasher.finalize(); 25 | 26 | let mut hash_bytes = [0u8; 32]; 27 | hash_bytes.copy_from_slice(hash.as_slice()); 28 | 29 | let rng = ChaChaRng::from_seed(hash_bytes); 30 | 31 | Self { rng } 32 | } 33 | 34 | pub fn rand_bytes(&mut self) -> [u8; 32] { 35 | let mut bytes = [0u8; 32]; 36 | self.rng.fill_bytes(&mut bytes); 37 | 38 | bytes 39 | } 40 | 41 | pub fn set_word_pos(&mut self, count: u32) { 42 | self.rng.set_word_pos(count.into()); 43 | } 44 | } 45 | 46 | impl RngCore for ContractPrng { 47 | fn next_u32(&mut self) -> u32 { 48 | self.rng.next_u32() 49 | } 50 | 51 | fn next_u64(&mut self) -> u64 { 52 | self.rng.next_u64() 53 | } 54 | 55 | fn fill_bytes(&mut self, dest: &mut [u8]) { 56 | self.rng.fill_bytes(dest) 57 | } 58 | 59 | fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), rand_core::Error> { 60 | self.rng.try_fill_bytes(dest) 61 | } 62 | } 63 | 64 | impl CryptoRng for ContractPrng {} 65 | 66 | #[cfg(test)] 67 | mod tests { 68 | use super::*; 69 | 70 | /// This test checks that the rng is stateful and generates 71 | /// different random bytes every time it is called. 72 | #[test] 73 | fn test_rng() { 74 | let mut rng = ContractPrng::new(b"foo", b"bar!"); 75 | let r1: [u8; 32] = [ 76 | 155, 11, 21, 97, 252, 65, 160, 190, 100, 126, 85, 251, 47, 73, 160, 49, 216, 182, 93, 77 | 30, 185, 67, 166, 22, 34, 10, 213, 112, 21, 136, 49, 214, 78 | ]; 79 | let r2: [u8; 32] = [ 80 | 46, 135, 19, 242, 111, 125, 59, 215, 114, 130, 122, 155, 202, 23, 36, 118, 83, 11, 6, 81 | 180, 97, 165, 218, 136, 134, 243, 191, 191, 149, 178, 7, 149, 82 | ]; 83 | let r3: [u8; 32] = [ 84 | 9, 2, 131, 50, 199, 170, 6, 68, 168, 28, 242, 182, 35, 114, 15, 163, 65, 139, 101, 221, 85 | 207, 147, 119, 110, 81, 195, 6, 134, 14, 253, 245, 244, 86 | ]; 87 | let r4: [u8; 32] = [ 88 | 68, 196, 114, 205, 225, 64, 201, 179, 18, 77, 216, 197, 211, 13, 21, 196, 11, 102, 106, 89 | 195, 138, 250, 29, 185, 51, 38, 183, 0, 5, 169, 65, 190, 90 | ]; 91 | assert_eq!(r1, rng.rand_bytes()); 92 | assert_eq!(r2, rng.rand_bytes()); 93 | assert_eq!(r3, rng.rand_bytes()); 94 | assert_eq!(r4, rng.rand_bytes()); 95 | } 96 | 97 | #[test] 98 | fn test_rand_bytes_counter() { 99 | let mut rng = ContractPrng::new(b"foo", b"bar"); 100 | 101 | let r1: [u8; 32] = [ 102 | 114, 227, 179, 76, 120, 34, 236, 42, 204, 27, 153, 74, 44, 29, 158, 162, 180, 202, 165, 103 | 46, 155, 90, 178, 252, 127, 80, 162, 79, 3, 146, 153, 88, 104 | ]; 105 | 106 | rng.set_word_pos(8); 107 | assert_eq!(r1, rng.rand_bytes()); 108 | rng.set_word_pos(8); 109 | assert_eq!(r1, rng.rand_bytes()); 110 | rng.set_word_pos(9); 111 | assert_ne!(r1, rng.rand_bytes()); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /packages/crypto/src/secp256k1.rs: -------------------------------------------------------------------------------- 1 | pub use secp256k1::constants::{COMPACT_SIGNATURE_SIZE as SIGNATURE_SIZE, MESSAGE_SIZE}; 2 | use secp256k1::ecdsa::Signature as SecpSignature; 3 | 4 | use cosmwasm_std::{Api, StdError}; 5 | 6 | pub const PRIVATE_KEY_SIZE: usize = secp256k1::constants::SECRET_KEY_SIZE; 7 | pub const PUBLIC_KEY_SIZE: usize = secp256k1::constants::UNCOMPRESSED_PUBLIC_KEY_SIZE; 8 | pub const COMPRESSED_PUBLIC_KEY_SIZE: usize = secp256k1::constants::PUBLIC_KEY_SIZE; 9 | 10 | pub struct PrivateKey { 11 | inner: secp256k1::SecretKey, 12 | } 13 | 14 | pub struct PublicKey { 15 | inner: secp256k1::PublicKey, 16 | } 17 | 18 | pub struct Signature { 19 | inner: SecpSignature, 20 | } 21 | 22 | impl PrivateKey { 23 | pub fn parse(raw: &[u8; PRIVATE_KEY_SIZE]) -> Result { 24 | secp256k1::SecretKey::from_slice(raw) 25 | .map(|key| PrivateKey { inner: key }) 26 | .map_err(|err| StdError::generic_err(format!("Error parsing PrivateKey: {err}"))) 27 | } 28 | 29 | pub fn serialize(&self) -> [u8; PRIVATE_KEY_SIZE] { 30 | self.inner.secret_bytes() 31 | } 32 | 33 | pub fn pubkey(&self) -> PublicKey { 34 | let secp = secp256k1::Secp256k1::new(); 35 | PublicKey { 36 | inner: secp256k1::PublicKey::from_secret_key(&secp, &self.inner), 37 | } 38 | } 39 | 40 | pub fn sign(&self, data: &[u8], api: A) -> Signature { 41 | let serialized_key = &self.serialize(); 42 | // will never fail since we guarantee that the inputs are valid. 43 | let sig_bytes = api.secp256k1_sign(data, serialized_key).unwrap(); 44 | let sig = SecpSignature::from_compact(&sig_bytes).unwrap(); 45 | 46 | Signature { inner: sig } 47 | } 48 | } 49 | 50 | impl PublicKey { 51 | pub fn parse(p: &[u8]) -> Result { 52 | secp256k1::PublicKey::from_slice(p) 53 | .map(|key| PublicKey { inner: key }) 54 | .map_err(|err| StdError::generic_err(format!("Error parsing PublicKey: {err}"))) 55 | } 56 | 57 | pub fn serialize(&self) -> [u8; PUBLIC_KEY_SIZE] { 58 | self.inner.serialize_uncompressed() 59 | } 60 | 61 | pub fn serialize_compressed(&self) -> [u8; COMPRESSED_PUBLIC_KEY_SIZE] { 62 | self.inner.serialize() 63 | } 64 | 65 | pub fn verify(&self, data: &[u8; MESSAGE_SIZE], signature: Signature, api: A) -> bool { 66 | let sig = &signature.serialize(); 67 | let pk = &self.serialize(); 68 | // will never fail since we guarantee that the inputs are valid. 69 | api.secp256k1_verify(data, sig, pk).unwrap() 70 | } 71 | } 72 | 73 | impl Signature { 74 | pub fn parse(p: &[u8; SIGNATURE_SIZE]) -> Result { 75 | SecpSignature::from_compact(p) 76 | .map(|sig| Signature { inner: sig }) 77 | .map_err(|err| StdError::generic_err(format!("Error parsing Signature: {err}"))) 78 | } 79 | 80 | pub fn parse_slice(p: &[u8]) -> Result { 81 | SecpSignature::from_compact(p) 82 | .map(|sig| Signature { inner: sig }) 83 | .map_err(|err| StdError::generic_err(format!("Error parsing Signature: {err}"))) 84 | } 85 | 86 | pub fn serialize(&self) -> [u8; SIGNATURE_SIZE] { 87 | self.inner.serialize_compact() 88 | } 89 | } 90 | 91 | #[cfg(test)] 92 | mod tests { 93 | use super::*; 94 | use crate::sha_256; 95 | use cosmwasm_std::testing::MockApi; 96 | use secp256k1::{rand::thread_rng, Secp256k1}; 97 | 98 | #[test] 99 | fn test_pubkey() { 100 | let s = Secp256k1::new(); 101 | let (secp_privkey, secp_pubkey) = s.generate_keypair(&mut thread_rng()); 102 | 103 | let mut privkey = [0u8; PRIVATE_KEY_SIZE]; 104 | privkey.copy_from_slice(&secp_privkey[..]); 105 | 106 | let new_pubkey = PrivateKey::parse(&privkey).unwrap().pubkey(); 107 | 108 | assert_eq!( 109 | new_pubkey.inner.serialize_uncompressed(), 110 | secp_pubkey.serialize_uncompressed() 111 | ); 112 | } 113 | 114 | #[test] 115 | fn test_sign() { 116 | let s = Secp256k1::new(); 117 | let (secp_privkey, _) = s.generate_keypair(&mut thread_rng()); 118 | let mock_api = MockApi::default(); 119 | 120 | let mut privkey = [0u8; PRIVATE_KEY_SIZE]; 121 | privkey.copy_from_slice(&secp_privkey[..]); 122 | 123 | let data = b"test"; 124 | let data_hash = sha_256(data); 125 | let pk = PrivateKey::parse(&privkey).unwrap(); 126 | let signature = pk.sign(data, mock_api); 127 | 128 | let pubkey = pk.pubkey(); 129 | assert!(pubkey.verify(&data_hash, signature, mock_api)); 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /packages/incubator/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "secret-toolkit-incubator" 3 | version = "0.10.3" 4 | edition = "2021" 5 | authors = ["SCRT Labs "] 6 | license-file = "../../LICENSE" 7 | repository = "https://github.com/scrtlabs/secret-toolkit" 8 | readme = "Readme.md" 9 | description = "Experimental tools for writing Secret Contracts" 10 | categories = ["cryptography::cryptocurrencies", "wasm"] 11 | keywords = ["secret-network", "secret-contracts", "secret-toolkit"] 12 | 13 | [package.metadata.docs.rs] 14 | all-features = true 15 | 16 | [dependencies] 17 | serde = { workspace = true, optional = true } 18 | cosmwasm-std = { workspace = true, optional = true } 19 | secret-toolkit-serialization = { version = "0.10.3", path = "../serialization", optional = true } 20 | 21 | [features] 22 | generational-store = ["secret-toolkit-serialization", "serde", "cosmwasm-std"] 23 | maxheap = ["secret-toolkit-serialization", "serde", "cosmwasm-std"] 24 | -------------------------------------------------------------------------------- /packages/incubator/Readme.md: -------------------------------------------------------------------------------- 1 | # Secret Contract Development Toolkit - Incubator 2 | 3 | ⚠️ This package is a sub-package of the `secret-toolkit` package. Please see its crate page for more context. 4 | 5 | This package contains tools that are not yet final and may change or contain unknown bugs, and are pending more testing or reviews. 6 | 7 | ## Max heap storage 8 | 9 | A "max heap store" is a storage wrapper that implements a binary tree maxheap data structure. 10 | 11 | Implementation based on 12 | 13 | * Insertion O(log n) 14 | * Remove max O(log n) 15 | 16 | ### Usage 17 | 18 | The usage of `MaxHeapStoreMut` and `MaxHeapStore` are modeled on `AppendStoreMut` and `AppendStore`, respectively. To add an item to the heap use `insert` and to take the top value off use `remove`, which also returns the item that was removed. To peek at the max value without removing, use the `get_max` function. Duplicate items can be added to the heap. 19 | 20 | ```rust 21 | # use cosmwasm_std::{StdError, testing::MockStorage}; 22 | # use secret_toolkit_incubator::maxheap::MaxHeapStoreMut; 23 | let mut storage = MockStorage::new(); 24 | let mut heap_store = MaxHeapStoreMut::attach_or_create(&mut storage)?; 25 | heap_store.insert(&1234)?; 26 | heap_store.insert(&2143)?; 27 | heap_store.insert(&4321)?; 28 | heap_store.insert(&3412)?; 29 | heap_store.insert(&2143)?; 30 | 31 | assert_eq!(heap_store.remove(), Ok(4321)); 32 | assert_eq!(heap_store.remove(), Ok(3412)); 33 | assert_eq!(heap_store.remove(), Ok(2143)); 34 | assert_eq!(heap_store.remove(), Ok(2143)); 35 | assert_eq!(heap_store.remove(), Ok(1234)); 36 | # Ok::<(), StdError>(()) 37 | ``` 38 | 39 | In order to use a custom struct with `MaxHeapStore` you will need to implement the appropriate Ordering traits. The following is an example with a custom struct `Tx` that uses the `amount` field to determine order in the heap: 40 | 41 | ```rust 42 | # use cosmwasm_std::{StdError, testing::MockStorage, Addr}; 43 | # use secret_toolkit_incubator::maxheap::MaxHeapStoreMut; 44 | # use serde::{Serialize, Deserialize}; 45 | # use std::cmp::Ordering; 46 | #[derive(Serialize, Deserialize, Clone, Debug, Eq)] 47 | pub struct Tx { 48 | address: Addr, 49 | amount: u128, 50 | } 51 | 52 | impl PartialOrd for Tx { 53 | fn partial_cmp(&self, other: &Self) -> Option { 54 | Some(self.cmp(other)) 55 | } 56 | } 57 | 58 | impl Ord for Tx { 59 | fn cmp(&self, other: &Self) -> Ordering { 60 | self.amount.cmp(&other.amount) 61 | } 62 | } 63 | 64 | impl PartialEq for Tx { 65 | fn eq(&self, other: &Self) -> bool { 66 | self.amount == other.amount 67 | } 68 | } 69 | 70 | let mut storage = MockStorage::new(); 71 | let mut heap_store = MaxHeapStoreMut::attach_or_create(&mut storage)?; 72 | 73 | heap_store.insert(&Tx{ 74 | address: Addr::unchecked("address1"), 75 | amount: 100, 76 | })?; 77 | heap_store.insert(&Tx{ 78 | address: Addr::unchecked("address2"), 79 | amount: 200, 80 | })?; 81 | heap_store.insert(&Tx{ 82 | address: Addr::unchecked("address5"), 83 | amount: 50, 84 | })?; 85 | 86 | assert_eq!(heap_store.remove(), Ok(Tx{ 87 | address: Addr::unchecked("address3"), 88 | amount: 200, 89 | })); 90 | assert_eq!(heap_store.remove(), Ok(Tx{ 91 | address: Addr::unchecked("address4"), 92 | amount: 100, 93 | })); 94 | assert_eq!(heap_store.remove(), Ok(Tx{ 95 | address: Addr::unchecked("address1"), 96 | amount: 50, 97 | })); 98 | # Ok::<(), StdError>(()) 99 | ``` 100 | 101 | `MaxHeapStore` is modeled on an `AppendStore` and stores the array representation of the heap in the same way, e.g. using `len` key to store the length. Therefore, you can attach an `AppendStore` to a max heap instead of `MaxHeapStore` if you want to iterate over all the values for some reason. 102 | 103 | ## Generational index storage 104 | 105 | Also known as a slot map, a generational index storage is an iterable data structure where each element in the list is identified by a unique key that is a pair (index, generation). Each time an item is removed from the list the generation of the storage increments by one. If a new item is placed at the same index as a previous item which had been removed previously, the old references will not point to the new element. This is because although the index matches, the generation does not. This ensures that each reference to an element in the list is stable and safe. 106 | 107 | Starting with an empty set, if we insert A we will have key: (index: 0, generation: 0). Inserting B will have the key: (index: 1, generation: 0). When we remove A the generation will increment by 1 and index 0 will be freed up. When we insert C it will go to the head of our list of free slots and be given the key (index: 0, generation: 1). If you attempt to get A the result will be None, even though A and C have both been at "0" position in the list. 108 | 109 | Unlike AppendStore, iteration over a generational index storage is not in order of insertion. 110 | 111 | ### Use cases 112 | 113 | The main use for this type of storage is when we want sets of elements that might be referenced by other structs or lists in a contract, and we want to ensure if an element is removed that our other references do not break. For example, imagine we have a contract where we want a collection of User structs that are independent of secret addresses (perhaps we want people to be able to move their accounts from one address to another). We also want people to be able to remove User accounts, so we use a generational index storage. We can reference the User account by its generational index key (index, generation). We can also reference relationships between users by adding a field in the User struct that points to another key in the generation index storage. If we remove a User and a new User is put in the same index but at a different generation, then there is no risk that the links will point to the wrong user. One can easily imagine this being expanded to a more heterogeneous group of inter-related elements, not just users. 114 | 115 | In effect, this example is a graph structure where the nodes are elements and the references to unique (index, generation) pairs are the edges. Any graph like that could be implemented using the generational index storage. 116 | 117 | ### Usage 118 | 119 | See tests in `generational_store.rs` for more examples, including iteration. 120 | 121 | ```rust 122 | # use cosmwasm_std::{StdError, testing::MockStorage}; 123 | # use secret_toolkit_incubator::generational_store::{GenerationalStoreMut, Index}; 124 | let mut storage = MockStorage::new(); 125 | let mut gen_store = GenerationalStoreMut::attach_or_create(&mut storage)?; 126 | let alpha = gen_store.insert(String::from("Alpha")); 127 | let beta = gen_store.insert(String::from("Beta")); 128 | let gamma = gen_store.insert(String::from("Gamma")); 129 | 130 | assert_eq!(gen_store.len(), 3_u32); 131 | assert_eq!(gen_store.remove(beta.clone()), Ok(Some(String::from("Beta")))); 132 | assert_eq!(gen_store.len(), 2_u32); 133 | assert_eq!(gen_store.get(alpha), Some(String::from("Alpha"))); 134 | assert_eq!(gen_store.get(beta.clone()), None); 135 | assert_eq!(gen_store.get(gamma), Some(String::from("Gamma"))); 136 | 137 | let delta = gen_store.insert(String::from("Delta")); 138 | assert_eq!(gen_store.get(delta.clone()), Some(String::from("Delta"))); 139 | // check that the generation has updated 140 | assert_ne!(delta.clone(), Index::from_raw_parts(1, 0)); 141 | // delta has filled the slot where beta was but generation is now 1 142 | assert_eq!(delta, Index::from_raw_parts(1, 1)); 143 | 144 | // cannot remove twice 145 | assert!(gen_store.remove(beta).is_err()); 146 | # Ok::<(), StdError>(()) 147 | ``` 148 | 149 | ### Todo 150 | 151 | Rename as SlotMap? (see: [https://docs.rs/slotmap/1.0.5/slotmap/](https://docs.rs/slotmap/1.0.5/slotmap/)) Simpler name though maybe not as evocative of what it actually does. 152 | -------------------------------------------------------------------------------- /packages/incubator/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![doc = include_str!("../Readme.md")] 2 | 3 | #[cfg(feature = "generational-store")] 4 | pub mod generational_store; 5 | #[cfg(feature = "generational-store")] 6 | pub use generational_store::{GenerationalStore, GenerationalStoreMut}; 7 | 8 | #[cfg(feature = "maxheap")] 9 | pub mod maxheap; 10 | #[cfg(feature = "maxheap")] 11 | pub use maxheap::{MaxHeapStore, MaxHeapStoreMut}; 12 | -------------------------------------------------------------------------------- /packages/incubator/src/maxheap.rs: -------------------------------------------------------------------------------- 1 | //! A "max heap store" is a storage wrapper that implements a binary tree maxheap data structure. 2 | //! https://en.wikipedia.org/wiki/Min-max_heap 3 | //! Implementation based on https://algorithmtutor.com/Data-Structures/Tree/Binary-Heaps/ 4 | //! 5 | //! Insertion O(log n) 6 | //! Remove max O(log n) 7 | //! 8 | use std::convert::TryInto; 9 | use std::marker::PhantomData; 10 | 11 | use serde::{de::DeserializeOwned, Serialize}; 12 | use std::cmp::PartialOrd; 13 | 14 | use cosmwasm_std::{StdError, StdResult, Storage}; 15 | 16 | use secret_toolkit_serialization::{Bincode2, Serde}; 17 | 18 | const LEN_KEY: &[u8] = b"len"; 19 | 20 | // Mutable maxheap store 21 | 22 | /// A type allowing both reads from and writes to the maxheap store at a given storage location. 23 | pub struct MaxHeapStoreMut<'a, T, Ser = Bincode2> 24 | where 25 | T: Serialize + DeserializeOwned + PartialOrd, 26 | Ser: Serde, 27 | { 28 | storage: &'a mut dyn Storage, 29 | item_type: PhantomData<*const T>, 30 | serialization_type: PhantomData<*const Ser>, 31 | len: u32, 32 | } 33 | 34 | impl<'a, T> MaxHeapStoreMut<'a, T, Bincode2> 35 | where 36 | T: Serialize + DeserializeOwned + PartialOrd, 37 | { 38 | /// Try to use the provided storage as an MaxHeapStore. If it doesn't seem to be one, then 39 | /// initialize it as one. 40 | /// 41 | /// Returns Err if the contents of the storage can not be parsed. 42 | pub fn attach_or_create(storage: &'a mut dyn Storage) -> StdResult { 43 | MaxHeapStoreMut::attach_or_create_with_serialization(storage, Bincode2) 44 | } 45 | 46 | /// Try to use the provided storage as an MaxHeapStore. 47 | /// 48 | /// Returns None if the provided storage doesn't seem like an MaxHeapStore. 49 | /// Returns Err if the contents of the storage can not be parsed. 50 | pub fn attach(storage: &'a mut dyn Storage) -> Option> { 51 | MaxHeapStoreMut::attach_with_serialization(storage, Bincode2) 52 | } 53 | } 54 | 55 | impl<'a, T, Ser> MaxHeapStoreMut<'a, T, Ser> 56 | where 57 | T: Serialize + DeserializeOwned + PartialOrd, 58 | Ser: Serde, 59 | { 60 | /// Try to use the provided storage as an MaxHeapStore. If it doesn't seem to be one, then 61 | /// initialize it as one. This method allows choosing the serialization format you want to use. 62 | /// 63 | /// Returns Err if the contents of the storage can not be parsed. 64 | pub fn attach_or_create_with_serialization( 65 | storage: &'a mut dyn Storage, 66 | _ser: Ser, 67 | ) -> StdResult { 68 | if let Some(len_vec) = storage.get(LEN_KEY) { 69 | Self::new(storage, &len_vec) 70 | } else { 71 | let len_vec = 0_u32.to_be_bytes(); 72 | storage.set(LEN_KEY, &len_vec); 73 | Self::new(storage, &len_vec) 74 | } 75 | } 76 | 77 | /// Try to use the provided storage as an MaxHeapStore. 78 | /// This method allows choosing the serialization format you want to use. 79 | /// 80 | /// Returns None if the provided storage doesn't seem like an MaxHeapStore. 81 | /// Returns Err if the contents of the storage can not be parsed. 82 | pub fn attach_with_serialization( 83 | storage: &'a mut dyn Storage, 84 | _ser: Ser, 85 | ) -> Option> { 86 | let len_vec = storage.get(LEN_KEY)?; 87 | Some(Self::new(storage, &len_vec)) 88 | } 89 | 90 | fn new(storage: &'a mut dyn Storage, len_vec: &[u8]) -> StdResult { 91 | let len_array = len_vec 92 | .try_into() 93 | .map_err(|err| StdError::parse_err("u32", err))?; 94 | let len = u32::from_be_bytes(len_array); 95 | 96 | Ok(Self { 97 | storage, 98 | item_type: PhantomData, 99 | serialization_type: PhantomData, 100 | len, 101 | }) 102 | } 103 | 104 | pub fn len(&self) -> u32 { 105 | self.len 106 | } 107 | 108 | pub fn is_empty(&self) -> bool { 109 | self.len == 0 110 | } 111 | 112 | pub fn storage(&mut self) -> &mut dyn Storage { 113 | self.storage 114 | } 115 | 116 | pub fn readonly_storage(&self) -> &dyn Storage { 117 | self.storage 118 | } 119 | 120 | /// Get the value stored at a given position. 121 | /// 122 | /// # Errors 123 | /// Will return an error if pos is out of bounds or if an item is not found. 124 | pub fn get_at(&self, pos: u32) -> StdResult { 125 | self.as_readonly().get_at(pos) 126 | } 127 | 128 | fn get_at_unchecked(&self, pos: u32) -> StdResult { 129 | self.as_readonly().get_at_unchecked(pos) 130 | } 131 | 132 | /// Set the value of the item stored at a given position. 133 | /// 134 | /// # Errors 135 | /// Will return an error if the position is out of bounds 136 | pub fn set_at(&mut self, pos: u32, item: &T) -> StdResult<()> { 137 | if pos >= self.len { 138 | return Err(StdError::generic_err("MaxHeapStore access out of bounds")); 139 | } 140 | self.set_at_unchecked(pos, item) 141 | } 142 | 143 | fn set_at_unchecked(&mut self, pos: u32, item: &T) -> StdResult<()> { 144 | let serialized = Ser::serialize(item)?; 145 | self.storage.set(&pos.to_be_bytes(), &serialized); 146 | Ok(()) 147 | } 148 | 149 | /// return index of the parent node 150 | fn parent(&self, idx: u32) -> u32 { 151 | (idx - 1) / 2 152 | } 153 | 154 | /// return index of the left child 155 | fn left_child(&self, idx: u32) -> u32 { 156 | 2 * idx + 1 157 | } 158 | 159 | /// return index of the right child 160 | fn right_child(&self, idx: u32) -> u32 { 161 | 2 * idx + 2 162 | } 163 | 164 | /// inserts an item into the heap at the correct position O(log n) 165 | pub fn insert(&mut self, item: &T) -> StdResult<()> { 166 | self.set_at_unchecked(self.len, item)?; 167 | self.set_length(self.len + 1); 168 | 169 | let mut i = self.len - 1; 170 | while i != 0 { 171 | let parent_i = self.parent(i); 172 | let parent_val = self.get_at_unchecked(parent_i)?; 173 | let val = self.get_at_unchecked(i)?; 174 | if parent_val < val { 175 | // swap 176 | self.set_at_unchecked(parent_i, item)?; 177 | self.set_at_unchecked(i, &parent_val)?; 178 | } 179 | i = parent_i; 180 | } 181 | 182 | Ok(()) 183 | } 184 | 185 | /// moves the item at position idx into its correct position 186 | fn max_heapify(&mut self, idx: u32) -> StdResult<()> { 187 | // find left child node 188 | let left = self.left_child(idx); 189 | 190 | // find the right child node 191 | let right = self.right_child(idx); 192 | 193 | // find the largest among 3 nodes 194 | let mut largest = idx; 195 | 196 | // check if the left node is larger than the current node 197 | if left <= self.len() && self.get_at_unchecked(left)? > self.get_at_unchecked(largest)? { 198 | largest = left; 199 | } 200 | 201 | // check if the right node is larger than the current node 202 | if right <= self.len() && self.get_at_unchecked(right)? > self.get_at_unchecked(largest)? { 203 | largest = right; 204 | } 205 | 206 | // swap the largest node with the current node 207 | // and repeat this process until the current node is larger than 208 | // the right and the left node 209 | if largest != idx { 210 | let temp: T = self.get_at_unchecked(idx)?; 211 | self.set_at_unchecked(idx, &self.get_at_unchecked(largest)?)?; 212 | self.set_at_unchecked(largest, &temp)?; 213 | self.max_heapify(largest)?; 214 | } 215 | 216 | Ok(()) 217 | } 218 | 219 | /// remove the max item and returns it 220 | pub fn remove(&mut self) -> StdResult { 221 | if let Some(len) = self.len.checked_sub(1) { 222 | let max_item = self.get_max()?; 223 | 224 | // replace the first item with the last item 225 | self.set_at_unchecked(0, &self.get_at_unchecked(len)?)?; 226 | self.set_length(len); 227 | 228 | // maintain the heap property by heapifying the first item 229 | self.max_heapify(0)?; 230 | 231 | Ok(max_item) 232 | } else { 233 | Err(StdError::generic_err("Can not pop from empty MaxHeap")) 234 | } 235 | } 236 | 237 | /// returns the maximum item in heap 238 | pub fn get_max(&self) -> StdResult { 239 | self.as_readonly().get_max() 240 | } 241 | 242 | /// Set the length of the collection 243 | fn set_length(&mut self, len: u32) { 244 | self.storage.set(LEN_KEY, &len.to_be_bytes()); 245 | self.len = len; 246 | } 247 | 248 | /// Gain access to the implementation of the immutable methods 249 | fn as_readonly(&self) -> MaxHeapStore { 250 | MaxHeapStore { 251 | storage: self.storage, 252 | item_type: self.item_type, 253 | serialization_type: self.serialization_type, 254 | len: self.len, 255 | } 256 | } 257 | } 258 | 259 | // Readonly maxheap store 260 | 261 | /// A type allowing only reads from an max heap store. useful in the context of queries. 262 | pub struct MaxHeapStore<'a, T, Ser = Bincode2> 263 | where 264 | T: Serialize + DeserializeOwned + PartialOrd, 265 | Ser: Serde, 266 | { 267 | storage: &'a dyn Storage, 268 | item_type: PhantomData<*const T>, 269 | serialization_type: PhantomData<*const Ser>, 270 | len: u32, 271 | } 272 | 273 | impl<'a, T> MaxHeapStore<'a, T, Bincode2> 274 | where 275 | T: Serialize + DeserializeOwned + PartialOrd, 276 | { 277 | /// Try to use the provided storage as a MaxHeapStore. 278 | /// 279 | /// Returns None if the provided storage doesn't seem like a MaxHeapStore. 280 | /// Returns Err if the contents of the storage can not be parsed. 281 | pub fn attach(storage: &'a dyn Storage) -> Option> { 282 | MaxHeapStore::attach_with_serialization(storage, Bincode2) 283 | } 284 | } 285 | 286 | impl<'a, T, Ser> MaxHeapStore<'a, T, Ser> 287 | where 288 | T: Serialize + DeserializeOwned + PartialOrd, 289 | Ser: Serde, 290 | { 291 | /// Try to use the provided storage as an MaxHeapStore. 292 | /// This method allows choosing the serialization format you want to use. 293 | /// 294 | /// Returns None if the provided storage doesn't seem like an MaxHeapStore. 295 | /// Returns Err if the contents of the storage can not be parsed. 296 | pub fn attach_with_serialization( 297 | storage: &'a dyn Storage, 298 | _ser: Ser, 299 | ) -> Option> { 300 | let len_vec = storage.get(LEN_KEY)?; 301 | Some(MaxHeapStore::new(storage, len_vec)) 302 | } 303 | 304 | fn new(storage: &'a dyn Storage, len_vec: Vec) -> StdResult { 305 | let len_array = len_vec 306 | .as_slice() 307 | .try_into() 308 | .map_err(|err| StdError::parse_err("u32", err))?; 309 | let len = u32::from_be_bytes(len_array); 310 | 311 | Ok(Self { 312 | storage, 313 | item_type: PhantomData, 314 | serialization_type: PhantomData, 315 | len, 316 | }) 317 | } 318 | 319 | pub fn len(&self) -> u32 { 320 | self.len 321 | } 322 | 323 | pub fn is_empty(&self) -> bool { 324 | self.len == 0 325 | } 326 | 327 | pub fn readonly_storage(&self) -> &'a dyn Storage { 328 | self.storage 329 | } 330 | 331 | /// Get the value stored at a given position. 332 | /// 333 | /// # Errors 334 | /// Will return an error if pos is out of bounds or if an item is not found. 335 | pub fn get_at(&self, pos: u32) -> StdResult { 336 | if pos >= self.len { 337 | return Err(StdError::generic_err("MaxHeapStore access out of bounds")); 338 | } 339 | self.get_at_unchecked(pos) 340 | } 341 | 342 | fn get_at_unchecked(&self, pos: u32) -> StdResult { 343 | let serialized = self.storage.get(&pos.to_be_bytes()).ok_or_else(|| { 344 | StdError::generic_err(format!("No item in MaxHeapStore at position {pos}")) 345 | })?; 346 | Ser::deserialize(&serialized) 347 | } 348 | 349 | /// returns the maximum item in heap 350 | pub fn get_max(&self) -> StdResult { 351 | self.get_at(0) 352 | } 353 | } 354 | 355 | #[cfg(test)] 356 | mod tests { 357 | use cosmwasm_std::testing::MockStorage; 358 | use serde::Deserialize; 359 | 360 | use secret_toolkit_serialization::Json; 361 | use std::cmp::Ordering; 362 | 363 | use super::*; 364 | 365 | #[test] 366 | fn test_insert_remove() -> StdResult<()> { 367 | let mut storage = MockStorage::new(); 368 | let mut heap_store = MaxHeapStoreMut::attach_or_create(&mut storage)?; 369 | heap_store.insert(&1234)?; 370 | heap_store.insert(&2143)?; 371 | heap_store.insert(&4321)?; 372 | heap_store.insert(&3412)?; 373 | heap_store.insert(&2143)?; 374 | 375 | assert_eq!(heap_store.remove(), Ok(4321)); 376 | assert_eq!(heap_store.remove(), Ok(3412)); 377 | assert_eq!(heap_store.remove(), Ok(2143)); 378 | assert_eq!(heap_store.remove(), Ok(2143)); 379 | assert_eq!(heap_store.remove(), Ok(1234)); 380 | assert!(heap_store.remove().is_err()); 381 | 382 | heap_store.insert(&1234)?; 383 | assert_eq!(heap_store.remove(), Ok(1234)); 384 | 385 | Ok(()) 386 | } 387 | 388 | #[test] 389 | fn test_custom_ord() -> StdResult<()> { 390 | #[derive(Serialize, Deserialize, Clone, Debug, Eq)] 391 | pub struct Tx { 392 | address: String, 393 | amount: u128, 394 | } 395 | 396 | impl PartialOrd for Tx { 397 | fn partial_cmp(&self, other: &Self) -> Option { 398 | Some(self.cmp(other)) 399 | } 400 | } 401 | 402 | impl Ord for Tx { 403 | fn cmp(&self, other: &Self) -> Ordering { 404 | self.amount.cmp(&other.amount) 405 | } 406 | } 407 | 408 | impl PartialEq for Tx { 409 | fn eq(&self, other: &Self) -> bool { 410 | self.amount == other.amount 411 | } 412 | } 413 | 414 | let mut storage = MockStorage::new(); 415 | let mut heap_store = MaxHeapStoreMut::attach_or_create(&mut storage)?; 416 | 417 | heap_store.insert(&Tx { 418 | address: "address1".to_string(), 419 | amount: 200, 420 | })?; 421 | heap_store.insert(&Tx { 422 | address: "address2".to_string(), 423 | amount: 100, 424 | })?; 425 | heap_store.insert(&Tx { 426 | address: "address3".to_string(), 427 | amount: 400, 428 | })?; 429 | heap_store.insert(&Tx { 430 | address: "address4".to_string(), 431 | amount: 300, 432 | })?; 433 | heap_store.insert(&Tx { 434 | address: "address5".to_string(), 435 | amount: 50, 436 | })?; 437 | 438 | assert_eq!( 439 | heap_store.remove(), 440 | Ok(Tx { 441 | address: "address3".to_string(), 442 | amount: 400, 443 | }) 444 | ); 445 | assert_eq!( 446 | heap_store.remove(), 447 | Ok(Tx { 448 | address: "address4".to_string(), 449 | amount: 300, 450 | }) 451 | ); 452 | assert_eq!( 453 | heap_store.remove(), 454 | Ok(Tx { 455 | address: "address1".to_string(), 456 | amount: 200, 457 | }) 458 | ); 459 | assert_eq!( 460 | heap_store.remove(), 461 | Ok(Tx { 462 | address: "address2".to_string(), 463 | amount: 100, 464 | }) 465 | ); 466 | assert_eq!( 467 | heap_store.remove(), 468 | Ok(Tx { 469 | address: "address5".to_string(), 470 | amount: 50, 471 | }) 472 | ); 473 | Ok(()) 474 | } 475 | 476 | #[test] 477 | fn test_attach_to_wrong_location() { 478 | let mut storage = MockStorage::new(); 479 | assert!(MaxHeapStore::::attach(&storage).is_none()); 480 | assert!(MaxHeapStoreMut::::attach(&mut storage).is_none()); 481 | } 482 | 483 | #[test] 484 | fn test_serializations() -> StdResult<()> { 485 | // Check the default behavior is Bincode2 486 | let mut storage = MockStorage::new(); 487 | 488 | let mut heap_store = MaxHeapStoreMut::attach_or_create(&mut storage)?; 489 | heap_store.insert(&1234)?; 490 | 491 | let bytes = heap_store.readonly_storage().get(&0_u32.to_be_bytes()); 492 | assert_eq!(bytes, Some(vec![210, 4, 0, 0])); 493 | 494 | // Check that overriding the serializer with Json works 495 | let mut storage = MockStorage::new(); 496 | let mut heap_store = 497 | MaxHeapStoreMut::attach_or_create_with_serialization(&mut storage, Json)?; 498 | heap_store.insert(&1234)?; 499 | let bytes = heap_store.readonly_storage().get(&0_u32.to_be_bytes()); 500 | assert_eq!(bytes, Some(b"1234".to_vec())); 501 | 502 | Ok(()) 503 | } 504 | } 505 | -------------------------------------------------------------------------------- /packages/notification/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "secret-toolkit-notification" 3 | version = "0.10.3" 4 | edition = "2021" 5 | authors = ["darwinzer0","blake-regalia"] 6 | license-file = "../../LICENSE" 7 | repository = "https://github.com/scrtlabs/secret-toolkit" 8 | readme = "Readme.md" 9 | description = "Helper tools for SNIP-52 notifications in Secret Contracts" 10 | categories = ["cryptography::cryptocurrencies", "wasm"] 11 | keywords = ["secret-network", "secret-contracts", "secret-toolkit"] 12 | 13 | [package.metadata.docs.rs] 14 | all-features = true 15 | 16 | [dependencies] 17 | cosmwasm-std = { workspace = true, version = "1.0.0" } 18 | serde = { workspace = true } 19 | 20 | ripemd = { version = "0.1.3", default-features = false } 21 | schemars = { workspace = true } 22 | 23 | # rand_core = { version = "0.6.4", default-features = false } 24 | # rand_chacha = { version = "0.3.1", default-features = false } 25 | sha2 = "0.10.6" 26 | chacha20poly1305 = { version = "0.10.1", default-features = false, features = ["alloc", "rand_core"] } 27 | generic-array = "0.14.7" 28 | hkdf = "0.12.3" 29 | primitive-types = { version = "0.12.2", default-features = false } 30 | hex = "0.4.3" 31 | minicbor = "0.25.1" 32 | 33 | secret-toolkit-crypto = { version = "0.10.3", path = "../crypto", features = [ 34 | "hash", "hkdf" 35 | ] } 36 | -------------------------------------------------------------------------------- /packages/notification/Readme.md: -------------------------------------------------------------------------------- 1 | # Secret Contract Development Toolkit - SNIP52 (Private Push Notification) Interface 2 | 3 | ⚠️ This package is a sub-package of the `secret-toolkit` package. Please see its crate page for more context. 4 | 5 | These functions are meant to help you easily create notification channels for private push notifications in secret contracts (see [SNIP-52 Private Push Notification](https://github.com/SolarRepublic/SNIPs/blob/feat/snip-52/SNIP-52.md)). 6 | 7 | ### Implementing a `DirectChannel` struct 8 | 9 | Each notification channel will have a specified data format, which is defined by creating a struct that implements the `DirectChannel` trait, which has one method: `encode_cbor`. 10 | 11 | The following example illustrates how you might implement this for a channel called `my_channel` and notification data containing two fields: `sender` and `amount`. 12 | 13 | ```ignore 14 | use cosmwasm_std::{Api, StdError, StdResult}; 15 | use secret_toolkit::notification::{EncoderExt, CBL_ARRAY_SHORT, CBL_BIGNUM_U64, CBL_U8, Notification, DirectChannel, GroupChannel}; 16 | use serde::{Deserialize, Serialize}; 17 | use minicbor_ser as cbor; 18 | 19 | #[derive(Serialize, Debug, Deserialize, Clone)] 20 | pub struct MyNotification { 21 | pub sender: Addr, 22 | pub amount: u128, 23 | } 24 | 25 | impl DirectChannel for MyNotification { 26 | const CHANNEL_ID: &'static str = "my_channel"; 27 | const CDDL_SCHEMA: &'static str = "my_channel=[sender:bstr .size 20,amount:uint .size 8]"; 28 | const ELEMENTS: u64 = 2; 29 | const PAYLOAD_SIZE: usize = CBL_ARRAY_SHORT + CBL_BIGNUM_U64 + CBL_U8; 30 | 31 | fn encode_cbor(&self, api: &dyn Api, encoder: &mut Encoder<&mut [u8]>) -> StdResult<()> { 32 | // amount:biguint (8-byte uint) 33 | encoder.ext_u64_from_u128(self.amount)?; 34 | 35 | // sender:bstr (20-byte address) 36 | let sender_raw = api.addr_canonicalize(sender.as_str())?; 37 | encoder.ext_address(sender_raw)?; 38 | 39 | Ok(()) 40 | } 41 | } 42 | ``` 43 | 44 | 45 | ### Sending a TxHash notification 46 | 47 | To send a notification to a recipient you first create a new `Notification` struct passing in the address of the recipient along with the notification data you want to send. Then to turn it into a `TxHashNotification` execute the `to_txhash_notification` method on the `Notification` by passing in `deps.api`, `env`, and an internal `secret`, which is a randomly generated byte slice that has been stored previously in your contract during initialization. 48 | 49 | The following code snippet creates a notification for the above `my_channel` and adds it to the contract `Response` as a plaintext attribute. 50 | 51 | ```ignore 52 | let notification = Notification::new( 53 | recipient, 54 | MyNotification { 55 | sender, 56 | 1000_u128, 57 | } 58 | ) 59 | .to_txhash_notification(deps.api, &env, secret)?; 60 | 61 | // ... other code 62 | 63 | // add notification to response 64 | Ok(Response::new() 65 | .set_data(to_binary(&ExecuteAnswer::MyMessage { status: Success } )?) 66 | .add_attribute_plaintext( 67 | notification.id_plaintext(), 68 | notification.data_plaintext(), 69 | ) 70 | ) 71 | ``` 72 | 73 | -------------------------------------------------------------------------------- /packages/notification/src/cbor.rs: -------------------------------------------------------------------------------- 1 | use cosmwasm_std::{CanonicalAddr, StdError, StdResult}; 2 | use minicbor::{data as cbor_data, encode as cbor_encode, Encoder}; 3 | 4 | /// Length of encoding an arry header that holds less than 24 items 5 | pub const CBL_ARRAY_SHORT: usize = 1; 6 | 7 | /// Length of encoding an arry header that holds between 24 and 255 items 8 | pub const CBL_ARRAY_MEDIUM: usize = 2; 9 | 10 | /// Length of encoding an arry header that holds more than 255 items 11 | pub const CBL_ARRAY_LARGE: usize = 3; 12 | 13 | /// Length of encoding a u8 value that is less than 24 14 | pub const CBL_U8_LESS_THAN_24: usize = 1; 15 | 16 | /// Length of encoding a u8 value that is greater than or equal to 24 17 | pub const CBL_U8: usize = 1 + 1; 18 | 19 | /// Length of encoding a u16 value 20 | pub const CBL_U16: usize = 1 + 2; 21 | 22 | /// Length of encoding a u32 value 23 | pub const CBL_U32: usize = 1 + 4; 24 | 25 | /// Length of encoding a u53 value (the maximum safe integer size for javascript) 26 | pub const CBL_U53: usize = 1 + 8; 27 | 28 | /// Length of encoding a u64 value (with the bignum tag attached) 29 | pub const CBL_BIGNUM_U64: usize = 1 + 1 + 8; 30 | 31 | // Length of encoding a timestamp 32 | pub const CBL_TIMESTAMP: usize = 1 + 1 + 8; 33 | 34 | // Length of encoding a 20-byte canonical address 35 | pub const CBL_ADDRESS: usize = 1 + 20; 36 | 37 | /// Wraps the CBOR error to CosmWasm StdError 38 | pub fn cbor_to_std_error(_e: cbor_encode::Error) -> StdError { 39 | StdError::generic_err("CBOR encoding error") 40 | } 41 | 42 | /// Extends the minicbor encoder with wrapper functions that handle CBOR errors 43 | pub trait EncoderExt { 44 | fn ext_tag(&mut self, tag: cbor_data::IanaTag) -> StdResult<&mut Self>; 45 | 46 | fn ext_u8(&mut self, value: u8) -> StdResult<&mut Self>; 47 | fn ext_u32(&mut self, value: u32) -> StdResult<&mut Self>; 48 | fn ext_u64_from_u128(&mut self, value: u128) -> StdResult<&mut Self>; 49 | fn ext_address(&mut self, value: CanonicalAddr) -> StdResult<&mut Self>; 50 | fn ext_bytes(&mut self, value: &[u8]) -> StdResult<&mut Self>; 51 | fn ext_timestamp(&mut self, value: u64) -> StdResult<&mut Self>; 52 | } 53 | 54 | impl EncoderExt for Encoder { 55 | #[inline] 56 | fn ext_tag(&mut self, tag: cbor_data::IanaTag) -> StdResult<&mut Self> { 57 | self.tag(cbor_data::Tag::from(tag)) 58 | .map_err(cbor_to_std_error) 59 | } 60 | 61 | #[inline] 62 | fn ext_u8(&mut self, value: u8) -> StdResult<&mut Self> { 63 | self.u8(value).map_err(cbor_to_std_error) 64 | } 65 | 66 | #[inline] 67 | fn ext_u32(&mut self, value: u32) -> StdResult<&mut Self> { 68 | self.u32(value).map_err(cbor_to_std_error) 69 | } 70 | 71 | #[inline] 72 | fn ext_u64_from_u128(&mut self, value: u128) -> StdResult<&mut Self> { 73 | self.ext_tag(cbor_data::IanaTag::PosBignum)? 74 | .ext_bytes(&value.to_be_bytes()[8..]) 75 | } 76 | 77 | #[inline] 78 | fn ext_address(&mut self, value: CanonicalAddr) -> StdResult<&mut Self> { 79 | self.ext_bytes(value.as_slice()) 80 | } 81 | 82 | #[inline] 83 | fn ext_bytes(&mut self, value: &[u8]) -> StdResult<&mut Self> { 84 | self.bytes(value).map_err(cbor_to_std_error) 85 | } 86 | 87 | #[inline] 88 | fn ext_timestamp(&mut self, value: u64) -> StdResult<&mut Self> { 89 | self.ext_tag(cbor_data::IanaTag::Timestamp)? 90 | .u64(value) 91 | .map_err(cbor_to_std_error) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /packages/notification/src/cipher.rs: -------------------------------------------------------------------------------- 1 | use chacha20poly1305::{ 2 | aead::{AeadInPlace, KeyInit}, 3 | ChaCha20Poly1305, 4 | }; 5 | use cosmwasm_std::{StdError, StdResult}; 6 | use generic_array::GenericArray; 7 | 8 | pub fn cipher_data(key: &[u8], nonce: &[u8], plaintext: &[u8], aad: &[u8]) -> StdResult> { 9 | let cipher = ChaCha20Poly1305::new_from_slice(key) 10 | .map_err(|e| StdError::generic_err(format!("{:?}", e)))?; 11 | let mut buffer: Vec = plaintext.to_vec(); 12 | cipher 13 | .encrypt_in_place(GenericArray::from_slice(nonce), aad, &mut buffer) 14 | .map_err(|e| StdError::generic_err(format!("{:?}", e)))?; 15 | Ok(buffer) 16 | } 17 | 18 | pub fn xor_bytes(vec1: &[u8], vec2: &[u8]) -> Vec { 19 | vec1.iter().zip(vec2.iter()).map(|(&a, &b)| a ^ b).collect() 20 | } 21 | -------------------------------------------------------------------------------- /packages/notification/src/funcs.rs: -------------------------------------------------------------------------------- 1 | use crate::cipher_data; 2 | use cosmwasm_std::{Binary, CanonicalAddr, StdResult}; 3 | use hkdf::hmac::Mac; 4 | use secret_toolkit_crypto::{hkdf_sha_256, sha_256, HmacSha256}; 5 | 6 | pub const SEED_LEN: usize = 32; // 256 bits 7 | 8 | /// 9 | /// fn notification_id 10 | /// 11 | /// Returns a notification id for the given address and channel id. 12 | /// 13 | pub fn notification_id(seed: &Binary, channel: &str, tx_hash: &str) -> StdResult { 14 | // compute notification ID for this event 15 | let material = [ 16 | channel.as_bytes(), 17 | ":".as_bytes(), 18 | tx_hash.to_ascii_uppercase().as_bytes(), 19 | ] 20 | .concat(); 21 | 22 | // create HMAC from seed 23 | let mut mac: HmacSha256 = HmacSha256::new_from_slice(seed.0.as_slice()).unwrap(); 24 | 25 | // add material to input stream 26 | mac.update(material.as_slice()); 27 | 28 | // finalize the digest and convert to CW Binary 29 | Ok(Binary::from(mac.finalize().into_bytes().as_slice())) 30 | } 31 | 32 | /// 33 | /// fn encrypt_notification_data 34 | /// 35 | /// Returns encrypted bytes given plaintext bytes, address, and channel id. 36 | /// Optionally, can set block size (default 36). 37 | /// 38 | pub fn encrypt_notification_data( 39 | block_height: &u64, 40 | tx_hash: &String, 41 | seed: &Binary, 42 | channel: &str, 43 | plaintext: Vec, 44 | block_size: Option, 45 | ) -> StdResult { 46 | // pad the plaintext to the optionally given block size 47 | let mut padded_plaintext = plaintext.clone(); 48 | if let Some(size) = block_size { 49 | zero_pad_right(&mut padded_plaintext, size); 50 | } 51 | 52 | // take the last 12 bytes of the channel name's hash to create the channel ID 53 | let channel_id_bytes = sha_256(channel.as_bytes())[..12].to_vec(); 54 | 55 | // take the last 12 bytes of the tx hash (after hex-decoding) to use for salt 56 | let salt_bytes = hex::decode(tx_hash).unwrap()[..12].to_vec(); 57 | 58 | // generate nonce by XOR'ing channel ID with salt 59 | let nonce: Vec = channel_id_bytes 60 | .iter() 61 | .zip(salt_bytes.iter()) 62 | .map(|(&b1, &b2)| b1 ^ b2) 63 | .collect(); 64 | 65 | // secure this message by attaching the block height and tx hash to the additional authenticated data 66 | let aad = format!("{}:{}", block_height, tx_hash); 67 | 68 | // encrypt notification data for this event 69 | let tag_ciphertext = cipher_data( 70 | seed.0.as_slice(), 71 | nonce.as_slice(), 72 | padded_plaintext.as_slice(), 73 | aad.as_bytes(), 74 | )?; 75 | 76 | Ok(Binary::from(tag_ciphertext.clone())) 77 | } 78 | 79 | /// get the seed for a secret and given address 80 | pub fn get_seed(addr: &CanonicalAddr, secret: &[u8]) -> StdResult { 81 | let seed = hkdf_sha_256(&None, secret, addr.as_slice(), SEED_LEN)?; 82 | 83 | Ok(Binary::from(seed)) 84 | } 85 | 86 | /// take a Vec and pad it up to a multiple of `block_size`, using 0x00 at the end 87 | fn zero_pad_right(message: &mut Vec, block_size: usize) -> &mut Vec { 88 | let len = message.len(); 89 | let surplus = len % block_size; 90 | if surplus == 0 { 91 | return message; 92 | } 93 | 94 | let missing = block_size - surplus; 95 | message.reserve(missing); 96 | message.extend(std::iter::repeat(0x00).take(missing)); 97 | message 98 | } 99 | -------------------------------------------------------------------------------- /packages/notification/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![doc = include_str!("../Readme.md")] 2 | 3 | pub mod cbor; 4 | pub mod cipher; 5 | pub mod funcs; 6 | pub mod structs; 7 | pub use cbor::*; 8 | pub use cipher::*; 9 | pub use funcs::*; 10 | pub use structs::*; 11 | -------------------------------------------------------------------------------- /packages/notification/src/structs.rs: -------------------------------------------------------------------------------- 1 | use cosmwasm_std::{Addr, Api, Binary, Env, StdError, StdResult, Uint64}; 2 | use minicbor::Encoder; 3 | use schemars::JsonSchema; 4 | use serde::{Deserialize, Serialize}; 5 | 6 | use crate::{cbor_to_std_error, encrypt_notification_data, get_seed, notification_id}; 7 | 8 | #[derive(Serialize, Debug, Deserialize, Clone)] 9 | #[cfg_attr(test, derive(Eq, PartialEq))] 10 | pub struct Notification { 11 | /// Recipient address of the notification 12 | pub notification_for: Addr, 13 | /// Typed notification data 14 | pub data: T, 15 | } 16 | 17 | pub trait DirectChannel { 18 | const CHANNEL_ID: &'static str; 19 | const CDDL_SCHEMA: &'static str; 20 | const ELEMENTS: u64; 21 | const PAYLOAD_SIZE: usize; 22 | 23 | fn channel_id(&self) -> String { 24 | Self::CHANNEL_ID.to_string() 25 | } 26 | 27 | fn cddl_schema(&self) -> String { 28 | Self::CDDL_SCHEMA.to_string() 29 | } 30 | 31 | fn to_cbor(&self, api: &dyn Api) -> StdResult> { 32 | // dynamically allocate output buffer 33 | let mut buffer = vec![0u8; Self::PAYLOAD_SIZE]; 34 | 35 | // create CBOR encoder 36 | let mut encoder = Encoder::new(&mut buffer[..]); 37 | 38 | // encode number of elements 39 | encoder.array(Self::ELEMENTS).map_err(cbor_to_std_error)?; 40 | 41 | // encode CBOR data 42 | self.encode_cbor(api, &mut encoder)?; 43 | 44 | // return buffer (already right-padded with zero bytes) 45 | Ok(buffer) 46 | } 47 | 48 | /// CBOR encodes notification data into the encoder 49 | fn encode_cbor(&self, api: &dyn Api, encoder: &mut Encoder<&mut [u8]>) -> StdResult<()>; 50 | } 51 | 52 | impl Notification { 53 | pub fn new(notification_for: Addr, data: T) -> Self { 54 | Notification { 55 | notification_for, 56 | data, 57 | } 58 | } 59 | 60 | pub fn to_txhash_notification( 61 | &self, 62 | api: &dyn Api, 63 | env: &Env, 64 | secret: &[u8], 65 | block_size: Option, 66 | ) -> StdResult { 67 | // extract and normalize tx hash 68 | let tx_hash = env 69 | .transaction 70 | .clone() 71 | .ok_or(StdError::generic_err("no tx hash found"))? 72 | .hash 73 | .to_ascii_uppercase(); 74 | 75 | // canonicalize notification recipient address 76 | let notification_for_raw = api.addr_canonicalize(self.notification_for.as_str())?; 77 | 78 | // derive recipient's notification seed 79 | let seed = get_seed(¬ification_for_raw, secret)?; 80 | 81 | // derive notification id 82 | let id = notification_id(&seed, self.data.channel_id().as_str(), &tx_hash)?; 83 | 84 | // use CBOR to encode the data 85 | let cbor_data = self.data.to_cbor(api)?; 86 | 87 | // encrypt the receiver message 88 | let encrypted_data = encrypt_notification_data( 89 | &env.block.height, 90 | &tx_hash, 91 | &seed, 92 | self.data.channel_id().as_str(), 93 | cbor_data, 94 | block_size, 95 | )?; 96 | 97 | // enstruct 98 | Ok(TxHashNotification { id, encrypted_data }) 99 | } 100 | } 101 | 102 | #[derive(Serialize, Debug, Deserialize, Clone)] 103 | #[cfg_attr(test, derive(Eq, PartialEq))] 104 | pub struct TxHashNotification { 105 | pub id: Binary, 106 | pub encrypted_data: Binary, 107 | } 108 | 109 | impl TxHashNotification { 110 | pub fn id_plaintext(&self) -> String { 111 | format!("snip52:{}", self.id.to_base64()) 112 | } 113 | 114 | pub fn data_plaintext(&self) -> String { 115 | self.encrypted_data.to_base64() 116 | } 117 | } 118 | 119 | // types for channel info response 120 | 121 | #[derive(Serialize, Deserialize, JsonSchema, Clone, Debug)] 122 | pub struct ChannelInfoData { 123 | /// same as query input 124 | pub channel: String, 125 | /// "counter", "txhash", "bloom" 126 | pub mode: String, 127 | 128 | /// txhash / bloom fields only 129 | /// if txhash argument was given, this will be its computed Notification ID 130 | pub answer_id: Option, 131 | 132 | /// bloom fields only 133 | /// bloom filter parameters 134 | pub parameters: Option, 135 | /// bloom filter data 136 | pub data: Option, 137 | 138 | /// counter fields only 139 | /// current counter value 140 | pub counter: Option, 141 | /// the next Notification ID 142 | pub next_id: Option, 143 | 144 | /// counter / txhash field only 145 | /// optional CDDL schema definition string for the CBOR-encoded notification data 146 | pub cddl: Option, 147 | } 148 | 149 | #[derive(Serialize, Deserialize, JsonSchema, Clone, Debug)] 150 | pub struct BloomParameters { 151 | pub m: u32, 152 | pub k: u32, 153 | pub h: String, 154 | } 155 | 156 | #[derive(Serialize, Deserialize, JsonSchema, Clone, Debug)] 157 | pub struct Descriptor { 158 | pub r#type: String, 159 | pub version: String, 160 | pub packet_size: u32, 161 | pub data: StructDescriptor, 162 | } 163 | 164 | #[derive(Serialize, Deserialize, JsonSchema, Clone, Debug)] 165 | pub struct StructDescriptor { 166 | pub r#type: String, 167 | pub label: String, 168 | pub members: Vec, 169 | } 170 | 171 | #[derive(Serialize, Deserialize, JsonSchema, Clone, Debug)] 172 | pub struct FlatDescriptor { 173 | pub r#type: String, 174 | pub label: String, 175 | pub description: Option, 176 | } 177 | 178 | pub trait GroupChannel { 179 | const CHANNEL_ID: &'static str; 180 | const BLOOM_N: usize; 181 | const BLOOM_M: u32; 182 | const BLOOM_K: u32; 183 | const PACKET_SIZE: usize; 184 | 185 | const BLOOM_M_LOG2: u32 = Self::BLOOM_M.ilog2(); 186 | 187 | fn build_packet(&self, api: &dyn Api, data: &D) -> StdResult>; 188 | 189 | fn notifications(&self) -> &Vec>; 190 | } 191 | -------------------------------------------------------------------------------- /packages/permit/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "secret-toolkit-permit" 3 | version = "0.10.3" 4 | edition = "2021" 5 | authors = ["SCRT Labs "] 6 | license-file = "../../LICENSE" 7 | repository = "https://github.com/scrtlabs/secret-toolkit" 8 | readme = "Readme.md" 9 | description = "Boilerplate for using permits in Secret Contracts" 10 | categories = ["cryptography::cryptocurrencies", "wasm"] 11 | keywords = ["secret-network", "secret-contracts", "secret-toolkit"] 12 | 13 | [package.metadata.docs.rs] 14 | all-features = true 15 | 16 | [dependencies] 17 | cosmwasm-std = { workspace = true, version = "1.0.0" } 18 | serde = { workspace = true } 19 | ripemd = { version = "0.1.3", default-features = false } 20 | schemars = { workspace = true } 21 | bech32 = "0.9.1" 22 | remain = "0.2.8" 23 | secret-toolkit-crypto = { version = "0.10.3", path = "../crypto", features = [ 24 | "hash", 25 | ] } 26 | -------------------------------------------------------------------------------- /packages/permit/Readme.md: -------------------------------------------------------------------------------- 1 | # Secret Contract Development Toolkit - Permits 2 | 3 | ⚠️ This package is a sub-package of the `secret-toolkit` package. Please see its crate page for more context. 4 | 5 | Utils for implementing permits, used by SNIP20 & SNIP721. 6 | -------------------------------------------------------------------------------- /packages/permit/src/funcs.rs: -------------------------------------------------------------------------------- 1 | use cosmwasm_std::{to_binary, Binary, CanonicalAddr, Deps, StdError, StdResult}; 2 | use ripemd::{Digest, Ripemd160}; 3 | 4 | use crate::{Permissions, Permit, RevokedPermits, SignedPermit}; 5 | use bech32::{ToBase32, Variant}; 6 | use secret_toolkit_crypto::sha_256; 7 | 8 | pub fn validate( 9 | deps: Deps, 10 | storage_prefix: &str, 11 | permit: &Permit, 12 | current_token_address: String, 13 | hrp: Option<&str>, 14 | ) -> StdResult { 15 | let account_hrp = hrp.unwrap_or("secret"); 16 | 17 | if !permit.check_token(¤t_token_address) { 18 | return Err(StdError::generic_err(format!( 19 | "Permit doesn't apply to token {:?}, allowed tokens: {:?}", 20 | current_token_address.as_str(), 21 | permit 22 | .params 23 | .allowed_tokens 24 | .iter() 25 | .map(|a| a.as_str()) 26 | .collect::>() 27 | ))); 28 | } 29 | 30 | // Derive account from pubkey 31 | let pubkey = &permit.signature.pub_key.value; 32 | 33 | let base32_addr = pubkey_to_account(pubkey).0.as_slice().to_base32(); 34 | let account: String = bech32::encode(account_hrp, base32_addr, Variant::Bech32).unwrap(); 35 | 36 | // Validate permit_name 37 | let permit_name = &permit.params.permit_name; 38 | let is_permit_revoked = 39 | RevokedPermits::is_permit_revoked(deps.storage, storage_prefix, &account, permit_name); 40 | if is_permit_revoked { 41 | return Err(StdError::generic_err(format!( 42 | "Permit {:?} was revoked by account {:?}", 43 | permit_name, 44 | account.as_str() 45 | ))); 46 | } 47 | 48 | // Validate signature, reference: https://github.com/enigmampc/SecretNetwork/blob/f591ed0cb3af28608df3bf19d6cfb733cca48100/cosmwasm/packages/wasmi-runtime/src/crypto/secp256k1.rs#L49-L82 49 | let signed_bytes = to_binary(&SignedPermit::from_params(&permit.params))?; 50 | let signed_bytes_hash = sha_256(signed_bytes.as_slice()); 51 | 52 | let verified = deps 53 | .api 54 | .secp256k1_verify(&signed_bytes_hash, &permit.signature.signature.0, &pubkey.0) 55 | .map_err(|err| StdError::generic_err(err.to_string()))?; 56 | 57 | if !verified { 58 | return Err(StdError::generic_err( 59 | "Failed to verify signatures for the given permit", 60 | )); 61 | } 62 | 63 | Ok(account) 64 | } 65 | 66 | pub fn pubkey_to_account(pubkey: &Binary) -> CanonicalAddr { 67 | let mut hasher = Ripemd160::new(); 68 | hasher.update(sha_256(&pubkey.0)); 69 | CanonicalAddr(Binary(hasher.finalize().to_vec())) 70 | } 71 | 72 | #[cfg(test)] 73 | mod tests { 74 | use super::*; 75 | use crate::{PermitParams, PermitSignature, PubKey, TokenPermissions}; 76 | use cosmwasm_std::testing::mock_dependencies; 77 | 78 | #[test] 79 | fn test_verify_permit() { 80 | let deps = mock_dependencies(); 81 | 82 | //{"permit": {"params":{"chain_id":"pulsar-2","permit_name":"memo_secret1rf03820fp8gngzg2w02vd30ns78qkc8rg8dxaq","allowed_tokens":["secret1rf03820fp8gngzg2w02vd30ns78qkc8rg8dxaq"],"permissions":["history"]},"signature":{"pub_key":{"type":"tendermint/PubKeySecp256k1","value":"A5M49l32ZrV+SDsPnoRv8fH7ivNC4gEX9prvd4RwvRaL"},"signature":"hw/Mo3ZZYu1pEiDdymElFkuCuJzg9soDHw+4DxK7cL9rafiyykh7VynS+guotRAKXhfYMwCiyWmiznc6R+UlsQ=="}}} 83 | 84 | let token = "secret1rf03820fp8gngzg2w02vd30ns78qkc8rg8dxaq".to_string(); 85 | 86 | let permit: Permit = Permit{ 87 | params: PermitParams { 88 | allowed_tokens: vec![token.clone()], 89 | permit_name: "memo_secret1rf03820fp8gngzg2w02vd30ns78qkc8rg8dxaq".to_string(), 90 | chain_id: "pulsar-2".to_string(), 91 | permissions: vec![TokenPermissions::History] 92 | }, 93 | signature: PermitSignature { 94 | pub_key: PubKey { 95 | r#type: "tendermint/PubKeySecp256k1".to_string(), 96 | value: Binary::from_base64("A5M49l32ZrV+SDsPnoRv8fH7ivNC4gEX9prvd4RwvRaL").unwrap(), 97 | }, 98 | signature: Binary::from_base64("hw/Mo3ZZYu1pEiDdymElFkuCuJzg9soDHw+4DxK7cL9rafiyykh7VynS+guotRAKXhfYMwCiyWmiznc6R+UlsQ==").unwrap() 99 | } 100 | }; 101 | 102 | let address = validate::<_>( 103 | deps.as_ref(), 104 | "test", 105 | &permit, 106 | token.clone(), 107 | Some("secret"), 108 | ) 109 | .unwrap(); 110 | 111 | assert_eq!( 112 | address, 113 | "secret1399pyvvk3hvwgxwt3udkslsc5jl3rqv4yshfrl".to_string() 114 | ); 115 | 116 | let address = validate::<_>(deps.as_ref(), "test", &permit, token, Some("cosmos")).unwrap(); 117 | 118 | assert_eq!( 119 | address, 120 | "cosmos1399pyvvk3hvwgxwt3udkslsc5jl3rqv4x4rq7r".to_string() 121 | ); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /packages/permit/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![doc = include_str!("../Readme.md")] 2 | 3 | pub mod funcs; 4 | pub mod state; 5 | pub mod structs; 6 | 7 | pub use funcs::*; 8 | pub use state::*; 9 | pub use structs::*; 10 | -------------------------------------------------------------------------------- /packages/permit/src/state.rs: -------------------------------------------------------------------------------- 1 | use cosmwasm_std::Storage; 2 | 3 | pub struct RevokedPermits; 4 | 5 | impl RevokedPermits { 6 | pub fn is_permit_revoked( 7 | storgae: &dyn Storage, 8 | storage_prefix: &str, 9 | account: &str, 10 | permit_name: &str, 11 | ) -> bool { 12 | let storage_key = storage_prefix.to_string() + account + permit_name; 13 | 14 | storgae.get(storage_key.as_bytes()).is_some() 15 | } 16 | 17 | pub fn revoke_permit( 18 | storage: &mut dyn Storage, 19 | storage_prefix: &str, 20 | account: &str, 21 | permit_name: &str, 22 | ) { 23 | let storage_key = storage_prefix.to_string() + account + permit_name; 24 | 25 | // Since cosmwasm V1.0 it's not possible to set an empty value, hence set some unimportant 26 | // character '_' 27 | // 28 | // Here is the line of the new panic that was added when trying to insert an empty value: 29 | // https://github.com/scrtlabs/cosmwasm/blob/f7e2b1dbf11e113e258d796288752503a5012367/packages/std/src/storage.rs#L30 30 | storage.set(storage_key.as_bytes(), "_".as_bytes()) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/permit/src/structs.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::field_reassign_with_default)] // This is triggered in `#[derive(JsonSchema)]` 2 | 3 | use schemars::JsonSchema; 4 | use serde::{Deserialize, Serialize}; 5 | 6 | use crate::pubkey_to_account; 7 | use cosmwasm_std::{Binary, CanonicalAddr, Uint128}; 8 | 9 | #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] 10 | #[serde(rename_all = "snake_case")] 11 | pub struct Permit { 12 | #[serde(bound = "")] 13 | pub params: PermitParams, 14 | pub signature: PermitSignature, 15 | } 16 | 17 | impl Permit { 18 | pub fn check_token(&self, token: &str) -> bool { 19 | self.params.allowed_tokens.contains(&token.to_string()) 20 | } 21 | 22 | pub fn check_permission(&self, permission: &Permission) -> bool { 23 | self.params.permissions.contains(permission) 24 | } 25 | } 26 | 27 | #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] 28 | #[serde(rename_all = "snake_case")] 29 | pub struct PermitParams { 30 | pub allowed_tokens: Vec, 31 | pub permit_name: String, 32 | pub chain_id: String, 33 | #[serde(bound = "")] 34 | pub permissions: Vec, 35 | } 36 | 37 | #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] 38 | #[serde(rename_all = "snake_case")] 39 | pub struct PermitSignature { 40 | pub pub_key: PubKey, 41 | pub signature: Binary, 42 | } 43 | 44 | #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] 45 | #[serde(rename_all = "snake_case")] 46 | pub struct PubKey { 47 | /// ignored, but must be "tendermint/PubKeySecp256k1" otherwise the verification will fail 48 | pub r#type: String, 49 | /// Secp256k1 PubKey 50 | pub value: Binary, 51 | } 52 | 53 | impl PubKey { 54 | pub fn canonical_address(&self) -> CanonicalAddr { 55 | pubkey_to_account(&self.value) 56 | } 57 | } 58 | 59 | // Note: The order of fields in this struct is important for the permit signature verification! 60 | #[remain::sorted] 61 | #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] 62 | #[serde(rename_all = "snake_case")] 63 | pub struct SignedPermit { 64 | /// ignored 65 | pub account_number: Uint128, 66 | /// ignored, no Env in query 67 | pub chain_id: String, 68 | /// ignored 69 | pub fee: Fee, 70 | /// ignored 71 | pub memo: String, 72 | /// the signed message 73 | #[serde(bound = "")] 74 | pub msgs: Vec>, 75 | /// ignored 76 | pub sequence: Uint128, 77 | } 78 | 79 | impl SignedPermit { 80 | pub fn from_params(params: &PermitParams) -> Self { 81 | Self { 82 | account_number: Uint128::zero(), 83 | chain_id: params.chain_id.clone(), 84 | fee: Fee::new(), 85 | memo: String::new(), 86 | msgs: vec![PermitMsg::from_content(PermitContent::from_params(params))], 87 | sequence: Uint128::zero(), 88 | } 89 | } 90 | } 91 | 92 | // Note: The order of fields in this struct is important for the permit signature verification! 93 | #[remain::sorted] 94 | #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] 95 | #[serde(rename_all = "snake_case")] 96 | pub struct Fee { 97 | pub amount: Vec, 98 | pub gas: Uint128, 99 | } 100 | 101 | impl Fee { 102 | pub fn new() -> Self { 103 | Self { 104 | amount: vec![Coin::new()], 105 | gas: Uint128::new(1), 106 | } 107 | } 108 | } 109 | 110 | impl Default for Fee { 111 | fn default() -> Self { 112 | Self::new() 113 | } 114 | } 115 | 116 | // Note: The order of fields in this struct is important for the permit signature verification! 117 | #[remain::sorted] 118 | #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] 119 | #[serde(rename_all = "snake_case")] 120 | pub struct Coin { 121 | pub amount: Uint128, 122 | pub denom: String, 123 | } 124 | 125 | impl Coin { 126 | pub fn new() -> Self { 127 | Self { 128 | amount: Uint128::zero(), 129 | denom: "uscrt".to_string(), 130 | } 131 | } 132 | } 133 | 134 | impl Default for Coin { 135 | fn default() -> Self { 136 | Self::new() 137 | } 138 | } 139 | 140 | // Note: The order of fields in this struct is important for the permit signature verification! 141 | #[remain::sorted] 142 | #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] 143 | #[serde(rename_all = "snake_case")] 144 | pub struct PermitMsg { 145 | pub r#type: String, 146 | #[serde(bound = "")] 147 | pub value: PermitContent, 148 | } 149 | 150 | impl PermitMsg { 151 | pub fn from_content(content: PermitContent) -> Self { 152 | Self { 153 | r#type: "query_permit".to_string(), 154 | value: content, 155 | } 156 | } 157 | } 158 | 159 | // Note: The order of fields in this struct is important for the permit signature verification! 160 | #[remain::sorted] 161 | #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] 162 | #[serde(rename_all = "snake_case")] 163 | pub struct PermitContent { 164 | pub allowed_tokens: Vec, 165 | #[serde(bound = "")] 166 | pub permissions: Vec, 167 | pub permit_name: String, 168 | } 169 | 170 | impl PermitContent { 171 | pub fn from_params(params: &PermitParams) -> Self { 172 | Self { 173 | allowed_tokens: params.allowed_tokens.clone(), 174 | permit_name: params.permit_name.clone(), 175 | permissions: params.permissions.clone(), 176 | } 177 | } 178 | } 179 | 180 | /// This trait is an alias for all the other traits it inherits from. 181 | /// It does this by providing a blanket implementation for all types that 182 | /// implement the same set of traits 183 | pub trait Permissions: 184 | Clone + PartialEq + Serialize + for<'d> Deserialize<'d> + JsonSchema 185 | { 186 | } 187 | 188 | impl Permissions for T where 189 | T: Clone + PartialEq + Serialize + for<'d> Deserialize<'d> + JsonSchema 190 | { 191 | } 192 | 193 | #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] 194 | #[serde(rename_all = "snake_case")] 195 | pub enum TokenPermissions { 196 | /// Allowance for SNIP-20 - Permission to query allowance of the owner & spender 197 | Allowance, 198 | /// Balance for SNIP-20 - Permission to query balance 199 | Balance, 200 | /// History for SNIP-20 - Permission to query transfer_history & transaction_hisotry 201 | History, 202 | /// Owner permission indicates that the bearer of this permit should be granted all 203 | /// the access of the creator/signer of the permit. SNIP-721 uses this to grant 204 | /// viewing access to all data that the permit creator owns and is whitelisted for. 205 | /// For SNIP-721 use, a permit with Owner permission should NEVER be given to 206 | /// anyone else. If someone wants to share private data, they should whitelist 207 | /// the address they want to share with via a SetWhitelistedApproval tx, and that 208 | /// address will view the data by creating their own permit with Owner permission 209 | Owner, 210 | } 211 | -------------------------------------------------------------------------------- /packages/serialization/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "secret-toolkit-serialization" 3 | version = "0.10.3" 4 | edition = "2021" 5 | authors = ["SCRT Labs "] 6 | license-file = "../../LICENSE" 7 | repository = "https://github.com/scrtlabs/secret-toolkit" 8 | readme = "Readme.md" 9 | description = "Helper types for serialization in Secret Contracts" 10 | categories = ["cryptography::cryptocurrencies", "wasm"] 11 | keywords = ["secret-network", "secret-contracts", "secret-toolkit"] 12 | 13 | [package.metadata.docs.rs] 14 | all-features = true 15 | 16 | [features] 17 | default = ["json", "bincode2", "base64"] 18 | json = [] 19 | base64 = ["schemars"] 20 | 21 | [dependencies] 22 | serde = { workspace = true } 23 | bincode2 = { version = "2.0.1", optional = true } 24 | schemars = { workspace = true, optional = true } 25 | cosmwasm-std = { workspace = true, version = "1.0.0" } 26 | 27 | [dev-dependencies] 28 | serde_json = "1" 29 | cosmwasm-schema = { version = "1.0" } 30 | -------------------------------------------------------------------------------- /packages/serialization/Readme.md: -------------------------------------------------------------------------------- 1 | # Secret Contract Development Toolkit - Serialization Tools 2 | 3 | ⚠️ This package is a sub-package of the `secret-toolkit` package. Please see its crate page for more context. 4 | 5 | This package contains all the tools related to serialization helpers. 6 | -------------------------------------------------------------------------------- /packages/serialization/src/base64.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | use std::marker::PhantomData; 3 | 4 | use serde::{de, ser}; 5 | 6 | use cosmwasm_std::Binary; 7 | 8 | use crate::Serde; 9 | 10 | /// Alias of `cosmwasm_std::Binary` for better naming 11 | pub type Base64 = Binary; 12 | 13 | /// A wrapper that automatically deserializes base64 strings to `T` using 14 | /// one of the `Serde` types. 15 | /// Use it as a field of your Handle messages (input and output), for 16 | /// example in the `msg` field of the `Receive` interface, to remove the 17 | /// boilerplate of serializing or deserializing the `Binary` to the relevant 18 | /// type `T`. 19 | pub struct Base64Of { 20 | // This is pub so that users can easily unwrap this if needed, 21 | // or just swap the entire instance. 22 | pub inner: T, 23 | ser: PhantomData, 24 | } 25 | 26 | #[cfg(feature = "json")] 27 | pub type Base64JsonOf = Base64Of; 28 | 29 | #[cfg(feature = "bincode2")] 30 | pub type Base64Bincode2Of = Base64Of; 31 | 32 | impl From for Base64Of { 33 | fn from(other: T) -> Self { 34 | Self { 35 | inner: other, 36 | ser: PhantomData, 37 | } 38 | } 39 | } 40 | 41 | impl std::ops::Deref for Base64Of { 42 | type Target = T; 43 | fn deref(&self) -> &Self::Target { 44 | &self.inner 45 | } 46 | } 47 | 48 | impl std::ops::DerefMut for Base64Of { 49 | fn deref_mut(&mut self) -> &mut Self::Target { 50 | &mut self.inner 51 | } 52 | } 53 | 54 | impl ser::Serialize for Base64Of { 55 | fn serialize(&self, serializer: S) -> Result 56 | where 57 | S: ser::Serializer, 58 | { 59 | let string = match Ser::serialize(&self.inner) { 60 | Ok(b) => Binary(b).to_base64(), 61 | Err(err) => return Err(::custom(err)), 62 | }; 63 | serializer.serialize_str(&string) 64 | } 65 | } 66 | 67 | impl<'de, S: Serde, T: for<'des> de::Deserialize<'des>> de::Deserialize<'de> for Base64Of { 68 | fn deserialize(deserializer: D) -> Result 69 | where 70 | D: de::Deserializer<'de>, 71 | { 72 | deserializer.deserialize_str(Base64TVisitor::::new()) 73 | } 74 | } 75 | 76 | struct Base64TVisitor { 77 | inner: PhantomData, 78 | ser: PhantomData, 79 | } 80 | 81 | impl Base64TVisitor { 82 | fn new() -> Self { 83 | Self { 84 | inner: PhantomData, 85 | ser: PhantomData, 86 | } 87 | } 88 | } 89 | 90 | impl de::Deserialize<'des>> de::Visitor<'_> for Base64TVisitor { 91 | type Value = Base64Of; 92 | 93 | fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { 94 | formatter.write_str("valid base64 encoded string") 95 | } 96 | 97 | fn visit_str(self, v: &str) -> Result 98 | where 99 | E: de::Error, 100 | { 101 | let binary = Base64::from_base64(v).map_err(|_| { 102 | // 103 | E::custom(format!("invalid base64: {v}")) 104 | })?; 105 | match S::deserialize::(binary.as_slice()) { 106 | Ok(val) => Ok(Base64Of::from(val)), 107 | Err(err) => Err(E::custom(err)), 108 | } 109 | } 110 | } 111 | 112 | /// These traits are conditionally implemented for Base64Of 113 | /// if T implements the trait being implemented. 114 | mod passthrough_impls { 115 | use std::cmp::Ordering; 116 | use std::fmt::{Debug, Display, Formatter}; 117 | use std::hash::{Hash, Hasher}; 118 | use std::marker::PhantomData; 119 | 120 | use schemars::gen::SchemaGenerator; 121 | use schemars::schema::Schema; 122 | use schemars::JsonSchema; 123 | 124 | use cosmwasm_std::Binary; 125 | 126 | use crate::Serde; 127 | 128 | use super::Base64Of; 129 | 130 | // Clone 131 | impl Clone for Base64Of { 132 | fn clone(&self) -> Self { 133 | Self { 134 | inner: self.inner.clone(), 135 | ser: self.ser, 136 | } 137 | } 138 | } 139 | 140 | // Copy 141 | impl Copy for Base64Of {} 142 | 143 | // Debug 144 | impl Debug for Base64Of { 145 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 146 | self.inner.fmt(f) 147 | } 148 | } 149 | 150 | // Display 151 | impl Display for Base64Of { 152 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 153 | self.inner.fmt(f) 154 | } 155 | } 156 | 157 | // PartialEq 158 | impl PartialEq> for Base64Of { 159 | fn eq(&self, other: &Base64Of) -> bool { 160 | self.inner.eq(&other.inner) 161 | } 162 | } 163 | 164 | impl PartialEq for Base64Of { 165 | fn eq(&self, other: &T) -> bool { 166 | self.inner.eq(other) 167 | } 168 | } 169 | 170 | // Eq 171 | // This implementation is not possible because the `S: Ser` type parameter 172 | // shouldn't matter in the `PartialEq` implementation, but `Eq` demands 173 | // that Rhs is Self, and Rust doesn't recognize that the `PartialEq` impl 174 | // covers that case already. Basically it doesn't understand that S1 and S2 175 | // _can_ be the same type. 176 | // 177 | // impl Eq for Base64Of {} 178 | 179 | // PartialOrd 180 | impl PartialOrd> for Base64Of { 181 | fn partial_cmp(&self, other: &Base64Of) -> Option { 182 | self.inner.partial_cmp(&other.inner) 183 | } 184 | } 185 | 186 | impl PartialOrd for Base64Of { 187 | fn partial_cmp(&self, other: &T) -> Option { 188 | self.inner.partial_cmp(other) 189 | } 190 | } 191 | 192 | // Ord 193 | // This can not be implemented for the same reason that `Eq` can't be implemented. 194 | 195 | // Hash 196 | impl Hash for Base64Of { 197 | fn hash(&self, state: &mut H) { 198 | self.inner.hash(state) 199 | } 200 | } 201 | 202 | // Default 203 | impl Default for Base64Of { 204 | fn default() -> Self { 205 | Self { 206 | inner: T::default(), 207 | ser: PhantomData, 208 | } 209 | } 210 | } 211 | 212 | // JsonSchema 213 | impl JsonSchema for Base64Of { 214 | fn schema_name() -> String { 215 | Binary::schema_name() 216 | } 217 | 218 | fn json_schema(gen: &mut SchemaGenerator) -> Schema { 219 | Binary::json_schema(gen) 220 | } 221 | } 222 | } 223 | 224 | #[cfg(test)] 225 | mod test { 226 | use schemars::JsonSchema; 227 | use serde::{Deserialize, Serialize}; 228 | 229 | use cosmwasm_schema::schema_for; 230 | use cosmwasm_std::{Binary, StdResult}; 231 | 232 | use crate::base64::Base64JsonOf; 233 | 234 | #[derive(Deserialize, Serialize, PartialEq, Debug, JsonSchema)] 235 | struct Foo { 236 | bar: String, 237 | baz: u32, 238 | } 239 | 240 | impl Foo { 241 | fn new() -> Self { 242 | Self { 243 | bar: String::from("some stuff"), 244 | baz: 234, 245 | } 246 | } 247 | } 248 | 249 | #[derive(Deserialize, Serialize, PartialEq, Debug, JsonSchema)] 250 | struct Wrapper { 251 | inner: Base64JsonOf, 252 | } 253 | 254 | impl Wrapper { 255 | fn new() -> Self { 256 | Self { 257 | inner: Base64JsonOf::from(Foo::new()), 258 | } 259 | } 260 | } 261 | 262 | #[test] 263 | fn test_serialize() -> StdResult<()> { 264 | let serialized = cosmwasm_std::to_vec(&Base64JsonOf::from(Foo::new()))?; 265 | let serialized2 = 266 | cosmwasm_std::to_vec(&Binary::from(b"{\"bar\":\"some stuff\",\"baz\":234}"))?; 267 | assert_eq!( 268 | br#""eyJiYXIiOiJzb21lIHN0dWZmIiwiYmF6IjoyMzR9""#[..], 269 | serialized 270 | ); 271 | assert_eq!(serialized, serialized2); 272 | 273 | let serialized3 = cosmwasm_std::to_vec(&Wrapper::new())?; 274 | assert_eq!( 275 | br#"{"inner":"eyJiYXIiOiJzb21lIHN0dWZmIiwiYmF6IjoyMzR9"}"#[..], 276 | serialized3 277 | ); 278 | 279 | Ok(()) 280 | } 281 | 282 | #[test] 283 | fn test_deserialize() -> StdResult<()> { 284 | let obj: Base64JsonOf = 285 | cosmwasm_std::from_slice(&br#""eyJiYXIiOiJzb21lIHN0dWZmIiwiYmF6IjoyMzR9""#[..])?; 286 | assert_eq!(obj, Foo::new()); 287 | 288 | let obj2: Wrapper = cosmwasm_std::from_slice( 289 | &br#"{"inner":"eyJiYXIiOiJzb21lIHN0dWZmIiwiYmF6IjoyMzR9"}"#[..], 290 | )?; 291 | assert_eq!(obj2, Wrapper::new()); 292 | assert_eq!(obj2.inner, Foo::new()); 293 | 294 | Ok(()) 295 | } 296 | 297 | #[test] 298 | fn test_schema() { 299 | let schema = schema_for!(Wrapper); 300 | let pretty = serde_json::to_string_pretty(&schema).unwrap(); 301 | println!("{}", pretty); 302 | println!("{:#?}", schema); 303 | 304 | assert_eq!(schema.schema.metadata.unwrap().title.unwrap(), "Wrapper"); 305 | let object = schema.schema.object.unwrap(); 306 | let required = object.required; 307 | assert_eq!(required.len(), 1); 308 | assert!(required.contains("inner")); 309 | 310 | // This checks that the schema sees the Base64Of field as a Binary object 311 | if let schemars::schema::Schema::Object(ref obj) = object.properties["inner"] { 312 | assert_eq!(obj.reference.as_ref().unwrap(), "#/definitions/Binary") 313 | } else { 314 | panic!("unexpected schema"); 315 | } 316 | } 317 | } 318 | -------------------------------------------------------------------------------- /packages/serialization/src/bincode2.rs: -------------------------------------------------------------------------------- 1 | use std::any::type_name; 2 | 3 | use serde::{de::DeserializeOwned, Serialize}; 4 | 5 | use cosmwasm_std::{StdError, StdResult}; 6 | 7 | use crate::Serde; 8 | 9 | /// Use bincode2 for serialization. 10 | #[derive(Copy, Clone, Debug)] 11 | pub struct Bincode2; 12 | 13 | impl Serde for Bincode2 { 14 | fn serialize(obj: &T) -> StdResult> { 15 | bincode2::serialize(obj).map_err(|err| StdError::serialize_err(type_name::(), err)) 16 | } 17 | 18 | fn deserialize(data: &[u8]) -> StdResult { 19 | bincode2::deserialize(data).map_err(|err| StdError::parse_err(type_name::(), err)) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/serialization/src/json.rs: -------------------------------------------------------------------------------- 1 | use serde::{de::DeserializeOwned, Serialize}; 2 | 3 | use cosmwasm_std::StdResult; 4 | 5 | use crate::Serde; 6 | 7 | /// Use json for serialization 8 | #[derive(Copy, Clone, Debug)] 9 | pub struct Json; 10 | 11 | impl Serde for Json { 12 | fn serialize(obj: &T) -> StdResult> { 13 | cosmwasm_std::to_vec(obj) 14 | } 15 | 16 | fn deserialize(data: &[u8]) -> StdResult { 17 | cosmwasm_std::from_slice(data) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/serialization/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![doc = include_str!("../Readme.md")] 2 | 3 | use serde::{de::DeserializeOwned, Serialize}; 4 | 5 | use cosmwasm_std::StdResult; 6 | 7 | #[cfg(feature = "base64")] 8 | mod base64; 9 | #[cfg(feature = "bincode2")] 10 | mod bincode2; 11 | #[cfg(feature = "json")] 12 | mod json; 13 | 14 | #[cfg(all(feature = "bincode2", feature = "base64"))] 15 | pub use crate::base64::Base64Bincode2Of; 16 | #[cfg(all(feature = "json", feature = "base64"))] 17 | pub use crate::base64::Base64JsonOf; 18 | #[cfg(feature = "base64")] 19 | pub use crate::base64::{Base64, Base64Of}; 20 | 21 | #[cfg(feature = "bincode2")] 22 | pub use crate::bincode2::Bincode2; 23 | #[cfg(feature = "json")] 24 | pub use crate::json::Json; 25 | 26 | /// This trait represents the ability to both serialize and deserialize using a specific format. 27 | /// 28 | /// This is useful for types that want to have a default mode of serialization, but want 29 | /// to allow users to override it if they want to. 30 | /// 31 | /// It is intentionally simple at the moment to keep the implementation easy. 32 | pub trait Serde { 33 | fn serialize(obj: &T) -> StdResult>; 34 | fn deserialize(data: &[u8]) -> StdResult; 35 | } 36 | -------------------------------------------------------------------------------- /packages/snip20/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "secret-toolkit-snip20" 3 | version = "0.10.3" 4 | edition = "2021" 5 | authors = ["SCRT Labs "] 6 | license-file = "../../LICENSE" 7 | repository = "https://github.com/scrtlabs/secret-toolkit" 8 | readme = "Readme.md" 9 | description = "Boilerplate for using SNIP-20 contracts on Secret Network" 10 | categories = ["cryptography::cryptocurrencies", "wasm"] 11 | keywords = ["secret-network", "secret-contracts", "secret-toolkit"] 12 | 13 | [package.metadata.docs.rs] 14 | all-features = true 15 | 16 | [dependencies] 17 | serde = { workspace = true } 18 | schemars = { workspace = true } 19 | cosmwasm-std = { workspace = true } 20 | secret-toolkit-utils = { version = "0.10.3", path = "../utils" } 21 | -------------------------------------------------------------------------------- /packages/snip20/Readme.md: -------------------------------------------------------------------------------- 1 | # Secret Contract Development Toolkit - SNIP20 Interface 2 | 3 | ⚠️ This package is a sub-package of the `secret-toolkit` package. Please see its crate page for more context. 4 | 5 | These functions are meant to help you easily interact with SNIP20 compliant tokens. 6 | 7 | ## Handle Messages 8 | 9 | You can create a HandleMsg variant and call the `to_cosmos_msg` function to generate the CosmosMsg that should be pushed onto the InitResponse or HandleResponse `messages` Vec. 10 | 11 | Or you can call the individual function for each Handle message to generate the appropriate callback CosmosMsg. 12 | 13 | Example: 14 | 15 | ```rust 16 | # use cosmwasm_std::{Uint128, StdError, StdResult, CosmosMsg, Response}; 17 | # use secret_toolkit_snip20::{transfer_msg}; 18 | # 19 | # fn main() -> StdResult<()> { 20 | let recipient = "ADDRESS_TO_TRANSFER_TO".to_string(); 21 | let amount = Uint128::from(10000u128); 22 | let memo = Some("memo".to_string()); 23 | let padding = None; 24 | let block_size = 256; 25 | let callback_code_hash = "TOKEN_CONTRACT_CODE_HASH".to_string(); 26 | let contract_addr = "TOKEN_CONTRACT_ADDRESS".to_string(); 27 | 28 | let cosmos_msg = transfer_msg( 29 | recipient, 30 | amount, 31 | memo, 32 | padding, 33 | block_size, 34 | callback_code_hash, 35 | contract_addr, 36 | )?; 37 | 38 | let response = Ok(Response::new().add_message(cosmos_msg)); 39 | # response.map(|_r| ()) 40 | # } 41 | ``` 42 | 43 | All you have to do to call a SNIP-20 Handle function is call the appropriate toolkit function, and place the resulting `CosmosMsg` in the `messages` Vec of the InitResponse or HandleResponse. In this example, we are transferring 10000 (in the lowest denomination of the token) to the recipient address. We are not using the `padding` field of the Transfer message, but instead, we are padding the entire message to blocks of 256 bytes. 44 | 45 | You probably have also noticed that CreateViewingKey is not supported. This is because a contract can not see the viewing key that is returned because it has already finished executing by the time CreateViewingKey would be called. If a contract needs to have a viewing key, it must create its own sufficiently complex viewing key, and pass it as a parameter to SetViewingKey. You can see an example of creating a complex viewing key in the [Snip20 Reference Implementation](http://github.com/enigmampc/snip20-reference-impl). It is also highly recommended that you use the block_size padding option to mask the length of the viewing key your contract has generated. 46 | 47 | ## Queries 48 | 49 | These are the types that SNIP20 tokens can return from queries 50 | 51 | ```rust 52 | # use cosmwasm_std::{Uint128, Coin}; 53 | # use serde::Serialize; 54 | # 55 | # #[derive(Serialize)] 56 | pub struct TokenInfo { 57 | pub name: String, 58 | pub symbol: String, 59 | pub decimals: u8, 60 | #[serde(skip_serializing_if = "Option::is_none")] 61 | pub total_supply: Option, 62 | } 63 | 64 | pub struct ExchangeRate { 65 | pub rate: Uint128, 66 | pub denom: String, 67 | } 68 | 69 | # #[derive(Serialize)] 70 | pub struct Allowance { 71 | pub spender: String, 72 | pub owner: String, 73 | pub allowance: Uint128, 74 | #[serde(skip_serializing_if = "Option::is_none")] 75 | pub expiration: Option, 76 | } 77 | 78 | pub struct Balance { 79 | pub amount: Uint128, 80 | } 81 | 82 | # #[derive(Serialize)] 83 | pub struct Tx { 84 | pub id: u64, 85 | pub from: String, 86 | pub sender: String, 87 | pub receiver: String, 88 | pub coins: Coin, 89 | #[serde(skip_serializing_if = "Option::is_none")] 90 | pub memo: Option, 91 | pub block_time: Option, 92 | pub block_height: Option, 93 | } 94 | 95 | pub struct TransferHistory { 96 | pub total: Option, 97 | pub txs: Vec, 98 | } 99 | 100 | # #[derive(Serialize)] 101 | #[serde(rename_all = "snake_case")] 102 | pub enum TxAction { 103 | Transfer { 104 | from: String, 105 | sender: String, 106 | recipient: String, 107 | }, 108 | Mint { 109 | minter: String, 110 | recipient: String, 111 | }, 112 | Burn { 113 | burner: String, 114 | owner: String, 115 | }, 116 | Deposit {}, 117 | Redeem {}, 118 | } 119 | 120 | # #[derive(Serialize)] 121 | pub struct RichTx { 122 | pub id: u64, 123 | pub action: TxAction, 124 | pub coins: Coin, 125 | #[serde(skip_serializing_if = "Option::is_none")] 126 | pub memo: Option, 127 | pub block_time: u64, 128 | pub block_height: u64, 129 | } 130 | 131 | pub struct TransactionHistory { 132 | pub total: Option, 133 | pub txs: Vec, 134 | } 135 | 136 | pub struct Minters { 137 | pub minters: Vec, 138 | } 139 | ``` 140 | 141 | You can create a QueryMsg variant and call the `query` function to query a SNIP20 token contract. 142 | 143 | Or you can call the individual function for each query. 144 | 145 | Example: 146 | 147 | ```rust 148 | # use cosmwasm_std::{StdError, QuerierWrapper, testing::mock_dependencies}; 149 | # use secret_toolkit_snip20::balance_query; 150 | # let mut deps = mock_dependencies(); 151 | # 152 | let address = "ADDRESS_WHOSE_BALANCE_IS_BEING_REQUESTED".to_string(); 153 | let key = "THE_VIEWING_KEY_PREVIOUSLY_SET_BY_THE_ADDRESS".to_string(); 154 | let block_size = 256; 155 | let callback_code_hash = "TOKEN_CONTRACT_CODE_HASH".to_string(); 156 | let contract_addr = "TOKEN_CONTRACT_ADDRESS".to_string(); 157 | 158 | let balance = 159 | balance_query(deps.as_ref().querier, address, key, block_size, callback_code_hash, contract_addr); 160 | # 161 | # assert_eq!( 162 | # balance.unwrap_err().to_string(), 163 | # "Generic error: Error performing Balance query: Generic error: Querier system error: No such contract: TOKEN_CONTRACT_ADDRESS" 164 | # ); 165 | ``` 166 | 167 | In this example, we are doing a Balance query for the specified address/key pair and storing the response in the balance variable, which is of the Balance type defined above. The query message is padded to blocks of 256 bytes. 168 | -------------------------------------------------------------------------------- /packages/snip20/src/batch.rs: -------------------------------------------------------------------------------- 1 | use schemars::JsonSchema; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | use cosmwasm_std::{Binary, Uint128}; 5 | 6 | #[derive(Serialize, Deserialize, JsonSchema, Clone, Debug, Eq, PartialEq)] 7 | #[serde(rename_all = "snake_case")] 8 | pub struct TransferAction { 9 | pub recipient: String, 10 | pub amount: Uint128, 11 | pub memo: Option, 12 | } 13 | 14 | impl TransferAction { 15 | pub fn new(recipient: String, amount: Uint128, memo: Option) -> Self { 16 | Self { 17 | recipient, 18 | amount, 19 | memo, 20 | } 21 | } 22 | } 23 | 24 | #[derive(Serialize, Deserialize, JsonSchema, Clone, Debug, Eq, PartialEq)] 25 | #[serde(rename_all = "snake_case")] 26 | pub struct SendAction { 27 | pub recipient: String, 28 | pub recipient_code_hash: Option, 29 | pub amount: Uint128, 30 | pub msg: Option, 31 | pub memo: Option, 32 | } 33 | 34 | impl SendAction { 35 | pub fn new( 36 | recipient: String, 37 | amount: Uint128, 38 | msg: Option, 39 | memo: Option, 40 | ) -> Self { 41 | Self { 42 | recipient, 43 | recipient_code_hash: None, 44 | amount, 45 | msg, 46 | memo, 47 | } 48 | } 49 | 50 | pub fn new_with_code_hash( 51 | recipient: String, 52 | recipient_code_hash: Option, 53 | amount: Uint128, 54 | msg: Option, 55 | memo: Option, 56 | ) -> Self { 57 | Self { 58 | recipient, 59 | recipient_code_hash, 60 | amount, 61 | msg, 62 | memo, 63 | } 64 | } 65 | } 66 | 67 | #[derive(Serialize, Deserialize, JsonSchema, Clone, Debug, Eq, PartialEq)] 68 | #[serde(rename_all = "snake_case")] 69 | pub struct TransferFromAction { 70 | pub owner: String, 71 | pub recipient: String, 72 | pub amount: Uint128, 73 | pub memo: Option, 74 | } 75 | 76 | impl TransferFromAction { 77 | pub fn new(owner: String, recipient: String, amount: Uint128, memo: Option) -> Self { 78 | Self { 79 | owner, 80 | recipient, 81 | amount, 82 | memo, 83 | } 84 | } 85 | } 86 | 87 | #[derive(Serialize, Deserialize, JsonSchema, Clone, Debug, Eq, PartialEq)] 88 | #[serde(rename_all = "snake_case")] 89 | pub struct SendFromAction { 90 | pub owner: String, 91 | pub recipient: String, 92 | pub recipient_code_hash: Option, 93 | pub amount: Uint128, 94 | pub msg: Option, 95 | pub memo: Option, 96 | } 97 | 98 | impl SendFromAction { 99 | pub fn new( 100 | owner: String, 101 | recipient: String, 102 | amount: Uint128, 103 | msg: Option, 104 | memo: Option, 105 | ) -> Self { 106 | Self { 107 | owner, 108 | recipient, 109 | recipient_code_hash: None, 110 | amount, 111 | msg, 112 | memo, 113 | } 114 | } 115 | 116 | pub fn new_with_code_hash( 117 | owner: String, 118 | recipient: String, 119 | recipient_code_hash: Option, 120 | amount: Uint128, 121 | msg: Option, 122 | memo: Option, 123 | ) -> Self { 124 | Self { 125 | owner, 126 | recipient, 127 | recipient_code_hash, 128 | amount, 129 | msg, 130 | memo, 131 | } 132 | } 133 | } 134 | 135 | #[derive(Serialize, Deserialize, JsonSchema, Clone, Debug, Eq, PartialEq)] 136 | #[serde(rename_all = "snake_case")] 137 | pub struct MintAction { 138 | pub recipient: String, 139 | pub amount: Uint128, 140 | pub memo: Option, 141 | } 142 | 143 | impl MintAction { 144 | pub fn new(recipient: String, amount: Uint128, memo: Option) -> Self { 145 | Self { 146 | recipient, 147 | amount, 148 | memo, 149 | } 150 | } 151 | } 152 | 153 | #[derive(Serialize, Deserialize, JsonSchema, Clone, Debug, Eq, PartialEq)] 154 | #[serde(rename_all = "snake_case")] 155 | pub struct BurnFromAction { 156 | pub owner: String, 157 | pub amount: Uint128, 158 | pub memo: Option, 159 | } 160 | 161 | impl BurnFromAction { 162 | pub fn new(owner: String, amount: Uint128, memo: Option) -> Self { 163 | Self { 164 | owner, 165 | amount, 166 | memo, 167 | } 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /packages/snip20/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![doc = include_str!("../Readme.md")] 2 | 3 | pub mod batch; 4 | pub mod handle; 5 | pub mod query; 6 | 7 | pub use handle::*; 8 | pub use query::*; 9 | -------------------------------------------------------------------------------- /packages/snip20/src/query.rs: -------------------------------------------------------------------------------- 1 | use core::fmt; 2 | use schemars::JsonSchema; 3 | use serde::{de::DeserializeOwned, Deserialize, Serialize}; 4 | 5 | use cosmwasm_std::{ 6 | to_binary, Coin, CustomQuery, QuerierWrapper, QueryRequest, StdError, StdResult, Uint128, 7 | WasmQuery, 8 | }; 9 | 10 | use secret_toolkit_utils::space_pad; 11 | 12 | /// TokenInfo response 13 | #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] 14 | pub struct TokenInfo { 15 | pub name: String, 16 | pub symbol: String, 17 | pub decimals: u8, 18 | #[serde(skip_serializing_if = "Option::is_none")] 19 | pub total_supply: Option, 20 | } 21 | 22 | /// TokenConfig response 23 | #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] 24 | pub struct TokenConfig { 25 | pub public_total_supply: bool, 26 | pub deposit_enabled: bool, 27 | pub redeem_enabled: bool, 28 | pub mint_enabled: bool, 29 | pub burn_enabled: bool, 30 | } 31 | 32 | /// Contract status 33 | #[derive(Serialize, Deserialize, Clone, PartialEq, Eq, JsonSchema, Debug)] 34 | pub enum ContractStatusLevel { 35 | NormalRun, 36 | StopAllButRedeems, 37 | StopAll, 38 | } 39 | 40 | /// ContractStatus Response 41 | #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] 42 | pub struct ContractStatus { 43 | pub status: ContractStatusLevel, 44 | } 45 | 46 | /// ExchangeRate response 47 | #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] 48 | pub struct ExchangeRate { 49 | pub rate: Uint128, 50 | pub denom: String, 51 | } 52 | 53 | /// Allowance response 54 | #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] 55 | pub struct Allowance { 56 | pub spender: String, 57 | pub owner: String, 58 | pub allowance: Uint128, 59 | #[serde(skip_serializing_if = "Option::is_none")] 60 | pub expiration: Option, 61 | } 62 | 63 | /// Balance response 64 | #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] 65 | pub struct Balance { 66 | pub amount: Uint128, 67 | } 68 | 69 | /// Transaction data 70 | #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] 71 | pub struct Tx { 72 | pub id: u64, 73 | pub from: String, 74 | pub sender: String, 75 | pub receiver: String, 76 | pub coins: Coin, 77 | #[serde(skip_serializing_if = "Option::is_none")] 78 | pub memo: Option, 79 | // The block time and block height are optional so that the JSON schema 80 | // reflects that some SNIP-20 contracts may not include this info. 81 | pub block_time: Option, 82 | pub block_height: Option, 83 | } 84 | 85 | /// TransferHistory response 86 | #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] 87 | pub struct TransferHistory { 88 | pub total: Option, 89 | pub txs: Vec, 90 | } 91 | 92 | /// Types of transactions for RichTx 93 | #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] 94 | #[serde(rename_all = "snake_case")] 95 | pub enum TxAction { 96 | Transfer { 97 | from: String, 98 | sender: String, 99 | recipient: String, 100 | }, 101 | Mint { 102 | minter: String, 103 | recipient: String, 104 | }, 105 | Burn { 106 | burner: String, 107 | owner: String, 108 | }, 109 | Deposit {}, 110 | Redeem {}, 111 | } 112 | 113 | /// Rich transaction data used for TransactionHistory 114 | #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] 115 | pub struct RichTx { 116 | pub id: u64, 117 | pub action: TxAction, 118 | pub coins: Coin, 119 | #[serde(skip_serializing_if = "Option::is_none")] 120 | pub memo: Option, 121 | pub block_time: u64, 122 | pub block_height: u64, 123 | } 124 | 125 | /// TransactionHistory response 126 | #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] 127 | pub struct TransactionHistory { 128 | pub total: Option, 129 | pub txs: Vec, 130 | } 131 | 132 | /// Minters response 133 | #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] 134 | pub struct Minters { 135 | pub minters: Vec, 136 | } 137 | 138 | /// SNIP20 queries 139 | #[derive(Serialize, Clone, Debug, Eq, PartialEq)] 140 | #[serde(rename_all = "snake_case")] 141 | pub enum QueryMsg { 142 | TokenInfo {}, 143 | TokenConfig {}, 144 | ContractStatus {}, 145 | ExchangeRate {}, 146 | Allowance { 147 | owner: String, 148 | spender: String, 149 | key: String, 150 | }, 151 | Balance { 152 | address: String, 153 | key: String, 154 | }, 155 | TransferHistory { 156 | address: String, 157 | key: String, 158 | page: Option, 159 | page_size: u32, 160 | }, 161 | TransactionHistory { 162 | address: String, 163 | key: String, 164 | page: Option, 165 | page_size: u32, 166 | }, 167 | Minters {}, 168 | } 169 | 170 | impl fmt::Display for QueryMsg { 171 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 172 | match *self { 173 | QueryMsg::TokenInfo { .. } => write!(f, "TokenInfo"), 174 | QueryMsg::TokenConfig { .. } => write!(f, "TokenConfig"), 175 | QueryMsg::ContractStatus { .. } => write!(f, "ContractStatus"), 176 | QueryMsg::ExchangeRate { .. } => write!(f, "ExchangeRate"), 177 | QueryMsg::Allowance { .. } => write!(f, "Allowance"), 178 | QueryMsg::Balance { .. } => write!(f, "Balance"), 179 | QueryMsg::TransferHistory { .. } => write!(f, "TransferHistory"), 180 | QueryMsg::TransactionHistory { .. } => write!(f, "TransactionHistory"), 181 | QueryMsg::Minters { .. } => write!(f, "Minters"), 182 | } 183 | } 184 | } 185 | 186 | impl QueryMsg { 187 | /// Returns a StdResult, where T is the "Response" type that wraps the query answer 188 | /// 189 | /// # Arguments 190 | /// 191 | /// * `querier` - a reference to the Querier dependency of the querying contract 192 | /// * `block_size` - pad the message to blocks of this size 193 | /// * `callback_code_hash` - String holding the code hash of the contract being queried 194 | /// * `contract_addr` - address of the contract being queried 195 | pub fn query( 196 | &self, 197 | querier: QuerierWrapper, 198 | mut block_size: usize, 199 | code_hash: String, 200 | contract_addr: String, 201 | ) -> StdResult { 202 | // can not have block size of 0 203 | if block_size == 0 { 204 | block_size = 1; 205 | } 206 | let mut msg = to_binary(self)?; 207 | space_pad(&mut msg.0, block_size); 208 | querier 209 | .query(&QueryRequest::Wasm(WasmQuery::Smart { 210 | contract_addr, 211 | code_hash, 212 | msg, 213 | })) 214 | .map_err(|err| StdError::generic_err(format!("Error performing {self} query: {err}"))) 215 | } 216 | } 217 | 218 | /// enum used to screen for a ViewingKeyError response from an authenticated query 219 | #[derive(Deserialize)] 220 | #[serde(rename_all = "snake_case")] 221 | pub enum AuthenticatedQueryResponse { 222 | Allowance { 223 | spender: String, 224 | owner: String, 225 | allowance: Uint128, 226 | expiration: Option, 227 | }, 228 | Balance { 229 | amount: Uint128, 230 | }, 231 | TransferHistory { 232 | txs: Vec, 233 | total: Option, 234 | }, 235 | TransactionHistory { 236 | txs: Vec, 237 | total: Option, 238 | }, 239 | ViewingKeyError { 240 | msg: String, 241 | }, 242 | } 243 | 244 | /// wrapper to deserialize TokenInfo response 245 | #[derive(Deserialize)] 246 | pub struct TokenInfoResponse { 247 | pub token_info: TokenInfo, 248 | } 249 | 250 | /// wrapper to deserialize TokenConfig response 251 | #[derive(Deserialize)] 252 | pub struct TokenConfigResponse { 253 | pub token_config: TokenConfig, 254 | } 255 | 256 | /// wrapper to deserialize ContractStatus response 257 | #[derive(Deserialize)] 258 | pub struct ContractStatusResponse { 259 | pub contract_status: ContractStatus, 260 | } 261 | 262 | /// wrapper to deserialize ExchangeRate response 263 | #[derive(Deserialize)] 264 | pub struct ExchangeRateResponse { 265 | pub exchange_rate: ExchangeRate, 266 | } 267 | 268 | /// wrapper to deserialize Minters response 269 | #[derive(Deserialize)] 270 | pub struct MintersResponse { 271 | pub minters: Minters, 272 | } 273 | 274 | /// Returns a StdResult from performing TokenInfo query 275 | /// 276 | /// # Arguments 277 | /// 278 | /// * `querier` - a reference to the Querier dependency of the querying contract 279 | /// * `block_size` - pad the message to blocks of this size 280 | /// * `callback_code_hash` - String holding the code hash of the contract being queried 281 | /// * `contract_addr` - address of the contract being queried 282 | pub fn token_info_query( 283 | querier: QuerierWrapper, 284 | block_size: usize, 285 | callback_code_hash: String, 286 | contract_addr: String, 287 | ) -> StdResult { 288 | let answer: TokenInfoResponse = 289 | QueryMsg::TokenInfo {}.query(querier, block_size, callback_code_hash, contract_addr)?; 290 | Ok(answer.token_info) 291 | } 292 | 293 | /// Returns a StdResult from performing TokenConfig query 294 | /// 295 | /// # Arguments 296 | /// 297 | /// * `querier` - a reference to the Querier dependency of the querying contract 298 | /// * `block_size` - pad the message to blocks of this size 299 | /// * `callback_code_hash` - String holding the code hash of the contract being queried 300 | /// * `contract_addr` - address of the contract being queried 301 | pub fn token_config_query( 302 | querier: QuerierWrapper, 303 | block_size: usize, 304 | callback_code_hash: String, 305 | contract_addr: String, 306 | ) -> StdResult { 307 | let answer: TokenConfigResponse = 308 | QueryMsg::TokenConfig {}.query(querier, block_size, callback_code_hash, contract_addr)?; 309 | Ok(answer.token_config) 310 | } 311 | 312 | /// Returns a StdResult from performing ContractStatus query 313 | /// 314 | /// # Arguments 315 | /// 316 | /// * `querier` - a reference to the Querier dependency of the querying contract 317 | /// * `block_size` - pad the message to blocks of this size 318 | /// * `callback_code_hash` - String holding the code hash of the contract being queried 319 | /// * `contract_addr` - address of the contract being queried 320 | pub fn contract_status_query( 321 | querier: QuerierWrapper, 322 | block_size: usize, 323 | callback_code_hash: String, 324 | contract_addr: String, 325 | ) -> StdResult { 326 | let answer: ContractStatusResponse = QueryMsg::ContractStatus {}.query( 327 | querier, 328 | block_size, 329 | callback_code_hash, 330 | contract_addr, 331 | )?; 332 | Ok(answer.contract_status) 333 | } 334 | 335 | /// Returns a StdResult from performing ExchangeRate query 336 | /// 337 | /// # Arguments 338 | /// 339 | /// * `querier` - a reference to the Querier dependency of the querying contract 340 | /// * `block_size` - pad the message to blocks of this size 341 | /// * `callback_code_hash` - String holding the code hash of the contract being queried 342 | /// * `contract_addr` - address of the contract being queried 343 | pub fn exchange_rate_query( 344 | querier: QuerierWrapper, 345 | block_size: usize, 346 | callback_code_hash: String, 347 | contract_addr: String, 348 | ) -> StdResult { 349 | let answer: ExchangeRateResponse = 350 | QueryMsg::ExchangeRate {}.query(querier, block_size, callback_code_hash, contract_addr)?; 351 | Ok(answer.exchange_rate) 352 | } 353 | 354 | /// Returns a StdResult from performing Allowance query 355 | /// 356 | /// # Arguments 357 | /// 358 | /// * `querier` - a reference to the Querier dependency of the querying contract 359 | /// * `owner` - the address that owns the tokens 360 | /// * `spender` - the address allowed to send/burn tokens 361 | /// * `key` - String holding the authentication key needed to view the allowance 362 | /// * `block_size` - pad the message to blocks of this size 363 | /// * `callback_code_hash` - String holding the code hash of the contract being queried 364 | /// * `contract_addr` - address of the contract being queried 365 | #[allow(clippy::too_many_arguments)] 366 | pub fn allowance_query( 367 | querier: QuerierWrapper, 368 | owner: String, 369 | spender: String, 370 | key: String, 371 | block_size: usize, 372 | callback_code_hash: String, 373 | contract_addr: String, 374 | ) -> StdResult { 375 | let answer: AuthenticatedQueryResponse = QueryMsg::Allowance { 376 | owner, 377 | spender, 378 | key, 379 | } 380 | .query(querier, block_size, callback_code_hash, contract_addr)?; 381 | match answer { 382 | AuthenticatedQueryResponse::Allowance { 383 | spender, 384 | owner, 385 | allowance, 386 | expiration, 387 | } => Ok(Allowance { 388 | spender, 389 | owner, 390 | allowance, 391 | expiration, 392 | }), 393 | AuthenticatedQueryResponse::ViewingKeyError { .. } => { 394 | Err(StdError::generic_err("unaithorized")) 395 | } 396 | _ => Err(StdError::generic_err("Invalid Allowance query response")), 397 | } 398 | } 399 | 400 | /// Returns a StdResult from performing Balance query 401 | /// 402 | /// # Arguments 403 | /// 404 | /// * `querier` - a reference to the Querier dependency of the querying contract 405 | /// * `address` - the address whose balance should be displayed 406 | /// * `key` - String holding the authentication key needed to view the balance 407 | /// * `block_size` - pad the message to blocks of this size 408 | /// * `callback_code_hash` - String holding the code hash of the contract being queried 409 | /// * `contract_addr` - address of the contract being queried 410 | pub fn balance_query( 411 | querier: QuerierWrapper, 412 | address: String, 413 | key: String, 414 | block_size: usize, 415 | callback_code_hash: String, 416 | contract_addr: String, 417 | ) -> StdResult { 418 | let answer: AuthenticatedQueryResponse = QueryMsg::Balance { address, key }.query( 419 | querier, 420 | block_size, 421 | callback_code_hash, 422 | contract_addr, 423 | )?; 424 | match answer { 425 | AuthenticatedQueryResponse::Balance { amount } => Ok(Balance { amount }), 426 | AuthenticatedQueryResponse::ViewingKeyError { .. } => { 427 | Err(StdError::generic_err("unaithorized")) 428 | } 429 | _ => Err(StdError::generic_err("Invalid Balance query response")), 430 | } 431 | } 432 | 433 | /// Returns a StdResult from performing TransferHistory query 434 | /// 435 | /// # Arguments 436 | /// 437 | /// * `querier` - a reference to the Querier dependency of the querying contract 438 | /// * `address` - the address whose transaction history should be displayed 439 | /// * `key` - String holding the authentication key needed to view transactions 440 | /// * `page` - Optional u32 representing the page number of transactions to display 441 | /// * `page_size` - u32 number of transactions to return 442 | /// * `block_size` - pad the message to blocks of this size 443 | /// * `callback_code_hash` - String holding the code hash of the contract being queried 444 | /// * `contract_addr` - address of the contract being queried 445 | #[allow(clippy::too_many_arguments)] 446 | pub fn transfer_history_query( 447 | querier: QuerierWrapper, 448 | address: String, 449 | key: String, 450 | page: Option, 451 | page_size: u32, 452 | block_size: usize, 453 | callback_code_hash: String, 454 | contract_addr: String, 455 | ) -> StdResult { 456 | let answer: AuthenticatedQueryResponse = QueryMsg::TransferHistory { 457 | address, 458 | key, 459 | page, 460 | page_size, 461 | } 462 | .query(querier, block_size, callback_code_hash, contract_addr)?; 463 | match answer { 464 | AuthenticatedQueryResponse::TransferHistory { txs, total } => { 465 | Ok(TransferHistory { txs, total }) 466 | } 467 | AuthenticatedQueryResponse::ViewingKeyError { .. } => { 468 | Err(StdError::generic_err("unaithorized")) 469 | } 470 | _ => Err(StdError::generic_err( 471 | "Invalid TransferHistory query response", 472 | )), 473 | } 474 | } 475 | 476 | /// Returns a StdResult from performing TransactionHistory query 477 | /// 478 | /// # Arguments 479 | /// 480 | /// * `querier` - a reference to the Querier dependency of the querying contract 481 | /// * `address` - the address whose transaction history should be displayed 482 | /// * `key` - String holding the authentication key needed to view transactions 483 | /// * `page` - Optional u32 representing the page number of transactions to display 484 | /// * `page_size` - u32 number of transactions to return 485 | /// * `block_size` - pad the message to blocks of this size 486 | /// * `callback_code_hash` - String holding the code hash of the contract being queried 487 | /// * `contract_addr` - address of the contract being queried 488 | #[allow(clippy::too_many_arguments)] 489 | pub fn transaction_history_query( 490 | querier: QuerierWrapper, 491 | address: String, 492 | key: String, 493 | page: Option, 494 | page_size: u32, 495 | block_size: usize, 496 | callback_code_hash: String, 497 | contract_addr: String, 498 | ) -> StdResult { 499 | let answer: AuthenticatedQueryResponse = QueryMsg::TransactionHistory { 500 | address, 501 | key, 502 | page, 503 | page_size, 504 | } 505 | .query(querier, block_size, callback_code_hash, contract_addr)?; 506 | match answer { 507 | AuthenticatedQueryResponse::TransactionHistory { txs, total } => { 508 | Ok(TransactionHistory { txs, total }) 509 | } 510 | AuthenticatedQueryResponse::ViewingKeyError { .. } => { 511 | Err(StdError::generic_err("unaithorized")) 512 | } 513 | _ => Err(StdError::generic_err( 514 | "Invalid TransactionHistory query response", 515 | )), 516 | } 517 | } 518 | 519 | /// Returns a StdResult from performing Minters query 520 | /// 521 | /// # Arguments 522 | /// 523 | /// * `querier` - a reference to the Querier dependency of the querying contract 524 | /// * `block_size` - pad the message to blocks of this size 525 | /// * `callback_code_hash` - String holding the code hash of the contract being queried 526 | /// * `contract_addr` - address of the contract being queried 527 | pub fn minters_query( 528 | querier: QuerierWrapper, 529 | block_size: usize, 530 | callback_code_hash: String, 531 | contract_addr: String, 532 | ) -> StdResult { 533 | let answer: MintersResponse = 534 | QueryMsg::Minters {}.query(querier, block_size, callback_code_hash, contract_addr)?; 535 | Ok(answer.minters) 536 | } 537 | -------------------------------------------------------------------------------- /packages/snip721/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "secret-toolkit-snip721" 3 | version = "0.10.3" 4 | edition = "2021" 5 | authors = ["SCRT Labs "] 6 | license-file = "../../LICENSE" 7 | repository = "https://github.com/scrtlabs/secret-toolkit" 8 | readme = "Readme.md" 9 | description = "Boilerplate for using SNIP-20 contracts on Secret Network" 10 | categories = ["cryptography::cryptocurrencies", "wasm"] 11 | keywords = ["secret-network", "secret-contracts", "secret-toolkit"] 12 | 13 | [package.metadata.docs.rs] 14 | all-features = true 15 | 16 | [dependencies] 17 | serde = { workspace = true } 18 | schemars = { workspace = true } 19 | cosmwasm-std = { workspace = true } 20 | secret-toolkit-utils = { version = "0.10.3", path = "../utils" } 21 | -------------------------------------------------------------------------------- /packages/snip721/Readme.md: -------------------------------------------------------------------------------- 1 | # Secret Contract Development Toolkit - SNIP-721 Interface 2 | 3 | ⚠️ This package is a sub-package of the `secret-toolkit` package. Please see its crate page for more context. 4 | 5 | These functions are meant to help you easily interact with SNIP-721 compliant NFT contracts. 6 | 7 | ## Handle Messages 8 | 9 | You can create a HandleMsg variant and call the `to_cosmos_msg` function to generate the CosmosMsg that should be pushed onto the InitResponse or HandleResponse `messages` Vec. 10 | 11 | Or you can call the individual function for each Handle message to generate the appropriate callback CosmosMsg. 12 | 13 | Example: 14 | 15 | ```rust 16 | # use cosmwasm_std::{Uint128, StdError, StdResult, CosmosMsg, Response}; 17 | # use secret_toolkit_snip721::transfer_nft_msg; 18 | # fn main() -> StdResult<()> { 19 | let recipient = "ADDRESS_TO_TRANSFER_TO".to_string(); 20 | let token_id = "TOKEN_ID".to_string(); 21 | let memo = Some("TRANSFER_MEMO".to_string()); 22 | let padding = None; 23 | let block_size = 256; 24 | let callback_code_hash = "TOKEN_CONTRACT_CODE_HASH".to_string(); 25 | let contract_addr = "TOKEN_CONTRACT_ADDRESS".to_string(); 26 | 27 | let cosmos_msg = transfer_nft_msg( 28 | recipient, 29 | token_id, 30 | memo, 31 | padding, 32 | block_size, 33 | callback_code_hash, 34 | contract_addr, 35 | )?; 36 | 37 | let response = Ok(Response::new().add_message(cosmos_msg)); 38 | # response.map(|_r| ()) 39 | # } 40 | ``` 41 | 42 | All you have to do to call a SNIP-721 Handle function is call the appropriate toolkit function, and place the resulting `CosmosMsg` in the `messages` Vec of the InitResponse or HandleResponse. In this example, we are transferring an NFT named "TOKEN_ID" to the recipient address. We are not using the `padding` field of the Transfer message, but instead, we are padding the entire message to blocks of 256 bytes. 43 | 44 | You probably have also noticed that CreateViewingKey is not supported. This is because a contract can not see the viewing key that is returned because it has already finished executing by the time CreateViewingKey would be called. If a contract needs to have a viewing key, it must create its own sufficiently complex viewing key, and pass it as a parameter to SetViewingKey. You can see an example of creating a complex viewing key in the [Snip20 Reference Implementation](http://github.com/enigmampc/snip20-reference-impl). It is also highly recommended that you use the block_size padding option to mask the length of the viewing key your contract has generated. 45 | 46 | ## Queries 47 | 48 | These are the types that the SNIP-721 toolkit queries can return 49 | 50 | ```rust 51 | # use secret_toolkit_snip721::{Expiration}; 52 | pub struct ContractInfo { 53 | pub name: String, 54 | pub symbol: String, 55 | } 56 | 57 | pub struct NumTokens { 58 | pub count: u32, 59 | } 60 | 61 | pub struct TokenList { 62 | pub tokens: Vec, 63 | } 64 | 65 | pub struct Cw721Approval { 66 | pub spender: String, 67 | pub expires: Expiration, 68 | } 69 | 70 | pub struct OwnerOf { 71 | pub owner: Option, 72 | pub approvals: Vec, 73 | } 74 | 75 | pub struct Metadata { 76 | pub name: Option, 77 | pub description: Option, 78 | pub image: Option, 79 | } 80 | 81 | pub struct AllNftInfo { 82 | pub access: OwnerOf, 83 | pub info: Option, 84 | } 85 | 86 | pub struct Snip721Approval { 87 | pub address: String, 88 | pub view_owner_expiration: Option, 89 | pub view_private_metadata_expiration: Option, 90 | pub transfer_expiration: Option, 91 | } 92 | 93 | pub struct NftDossier { 94 | pub owner: Option, 95 | pub public_metadata: Option, 96 | pub private_metadata: Option, 97 | pub display_private_metadata_error: Option, 98 | pub owner_is_public: bool, 99 | pub public_ownership_expiration: Option, 100 | pub private_metadata_is_public: bool, 101 | pub private_metadata_is_public_expiration: Option, 102 | pub token_approvals: Option>, 103 | pub inventory_approvals: Option>, 104 | } 105 | 106 | pub struct TokenApprovals { 107 | pub owner_is_public: bool, 108 | pub public_ownership_expiration: Option, 109 | pub private_metadata_is_public: bool, 110 | pub private_metadata_is_public_expiration: Option, 111 | pub token_approvals: Vec, 112 | } 113 | 114 | pub struct ApprovedForAll { 115 | pub operators: Vec, 116 | } 117 | 118 | pub struct InventoryApprovals { 119 | pub owner_is_public: bool, 120 | pub public_ownership_expiration: Option, 121 | pub private_metadata_is_public: bool, 122 | pub private_metadata_is_public_expiration: Option, 123 | pub inventory_approvals: Vec, 124 | } 125 | 126 | pub enum TxAction { 127 | Transfer { 128 | from: String, 129 | sender: Option, 130 | recipient: String, 131 | }, 132 | Mint { 133 | minter: String, 134 | recipient: String, 135 | }, 136 | Burn { 137 | owner: String, 138 | burner: Option, 139 | }, 140 | } 141 | 142 | pub struct Tx { 143 | pub tx_id: u64, 144 | pub block_height: u64, 145 | pub block_time: u64, 146 | pub token_id: String, 147 | pub action: TxAction, 148 | pub memo: Option, 149 | } 150 | 151 | pub struct TransactionHistory { 152 | pub total: u64, 153 | pub txs: Vec, 154 | } 155 | 156 | pub struct Minters { 157 | pub minters: Vec, 158 | } 159 | 160 | pub struct IsUnwrapped { 161 | pub token_is_unwrapped: bool, 162 | } 163 | 164 | pub struct VerifyTransferApproval { 165 | pub approved_for_all: bool, 166 | pub first_unapproved_token: Option, 167 | } 168 | ``` 169 | 170 | You can create a QueryMsg variant and call the `query` function to query a SNIP-721 token contract. 171 | 172 | Or you can call the individual function for each query. 173 | 174 | Example: 175 | 176 | ```rust 177 | # use cosmwasm_std::{StdError, testing::mock_dependencies}; 178 | # use secret_toolkit_snip721::{nft_dossier_query, ViewerInfo}; 179 | # let mut deps = mock_dependencies(); 180 | # 181 | let token_id = "TOKEN_ID".to_string(); 182 | let viewer = Some(ViewerInfo { 183 | address: "VIEWER'S_ADDRESS".to_string(), 184 | viewing_key: "VIEWER'S_KEY".to_string(), 185 | }); 186 | let include_expired = None; 187 | let block_size = 256; 188 | let callback_code_hash = "TOKEN_CONTRACT_CODE_HASH".to_string(); 189 | let contract_addr = "TOKEN_CONTRACT_ADDRESS".to_string(); 190 | 191 | let nft_dossier = nft_dossier_query( 192 | deps.as_ref().querier, 193 | token_id, 194 | viewer, 195 | include_expired, 196 | block_size, 197 | callback_code_hash, 198 | contract_addr 199 | ); 200 | # 201 | # assert_eq!( 202 | # nft_dossier.unwrap_err().to_string(), 203 | # "Generic error: Error performing NftDossier query: Generic error: Querier system error: No such contract: TOKEN_CONTRACT_ADDRESS" 204 | # ); 205 | ``` 206 | 207 | In this example, we are doing an NftDossier query on the token named "TOKEN_ID", supplying the address and viewing key of the querier, and storing the response in the nft_dossier variable, which is of the NftDossier type defined above. Because no `include_expired` was specified, the response defaults to only displaying approvals that have not expired, but approvals will only be displayed if the viewer is the owner of the token. The query message is padded to blocks of 256 bytes. 208 | -------------------------------------------------------------------------------- /packages/snip721/src/expiration.rs: -------------------------------------------------------------------------------- 1 | use schemars::JsonSchema; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | use cosmwasm_std::BlockInfo; 5 | use std::fmt; 6 | 7 | #[derive(Serialize, Deserialize, Clone, Copy, PartialEq, Eq, JsonSchema, Debug)] 8 | #[serde(rename_all = "snake_case")] 9 | /// at the given point in time and after, Expiration will be considered expired 10 | pub enum Expiration { 11 | /// expires at this block height 12 | AtHeight(u64), 13 | /// expires at the time in seconds since 01/01/1970 14 | AtTime(u64), 15 | /// never expires 16 | Never, 17 | } 18 | 19 | impl fmt::Display for Expiration { 20 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 21 | match self { 22 | Expiration::AtHeight(height) => write!(f, "expiration height: {height}"), 23 | Expiration::AtTime(time) => write!(f, "expiration time: {time}"), 24 | Expiration::Never => write!(f, "expiration: never"), 25 | } 26 | } 27 | } 28 | 29 | /// default is Never 30 | impl Default for Expiration { 31 | fn default() -> Self { 32 | Expiration::Never 33 | } 34 | } 35 | 36 | impl Expiration { 37 | /// Returns bool, true if Expiration has expired 38 | /// 39 | /// # Arguments 40 | /// 41 | /// * `block` - a reference to the BlockInfo containing the time to compare the Expiration to 42 | pub fn is_expired(&self, block: &BlockInfo) -> bool { 43 | match self { 44 | Expiration::AtHeight(height) => block.height >= *height, 45 | // When snip721 will be migrated, `time` might be a Timestamp. For now, just keeping it compatible 46 | Expiration::AtTime(time) => block.time.seconds() >= *time, 47 | Expiration::Never => false, 48 | } 49 | } 50 | } 51 | 52 | #[cfg(test)] 53 | mod test { 54 | use cosmwasm_std::Timestamp; 55 | 56 | use super::*; 57 | 58 | #[test] 59 | fn test_expiration() { 60 | let block_h1000_t1000000 = BlockInfo { 61 | height: 1000, 62 | time: Timestamp::from_seconds(1000000), 63 | chain_id: "test".to_string(), 64 | random: None, 65 | }; 66 | 67 | let block_h2000_t2000000 = BlockInfo { 68 | height: 2000, 69 | time: Timestamp::from_seconds(2000000), 70 | chain_id: "test".to_string(), 71 | random: None, 72 | }; 73 | let exp_h1000 = Expiration::AtHeight(1000); 74 | let exp_t1000000 = Expiration::AtTime(1000000); 75 | let exp_h1500 = Expiration::AtHeight(1500); 76 | let exp_t1500000 = Expiration::AtTime(1500000); 77 | let exp_never = Expiration::default(); 78 | 79 | assert!(exp_h1000.is_expired(&block_h1000_t1000000)); 80 | assert!(!exp_h1500.is_expired(&block_h1000_t1000000)); 81 | assert!(exp_h1500.is_expired(&block_h2000_t2000000)); 82 | assert!(!exp_never.is_expired(&block_h2000_t2000000)); 83 | assert!(exp_t1000000.is_expired(&block_h1000_t1000000)); 84 | assert!(!exp_t1500000.is_expired(&block_h1000_t1000000)); 85 | assert!(exp_t1500000.is_expired(&block_h2000_t2000000)); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /packages/snip721/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![doc = include_str!("../Readme.md")] 2 | 3 | //#![allow(clippy::field_reassign_with_default)] 4 | pub mod expiration; 5 | pub mod handle; 6 | pub mod metadata; 7 | pub mod query; 8 | 9 | pub use expiration::*; 10 | pub use handle::*; 11 | pub use metadata::*; 12 | pub use query::*; 13 | -------------------------------------------------------------------------------- /packages/snip721/src/metadata.rs: -------------------------------------------------------------------------------- 1 | use schemars::JsonSchema; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | // The response of both NftINfo and PrivateMetadata queries are Metadata 5 | // 6 | 7 | /// token metadata 8 | #[derive(Serialize, Deserialize, JsonSchema, Clone, PartialEq, Eq, Debug, Default)] 9 | pub struct Metadata { 10 | /// optional uri for off-chain metadata. This should be prefixed with `http://`, `https://`, `ipfs://`, or 11 | /// `ar://`. Only use this if you are not using `extension` 12 | pub token_uri: Option, 13 | /// optional on-chain metadata. Only use this if you are not using `token_uri` 14 | pub extension: Option, 15 | } 16 | 17 | /// metadata extension 18 | /// You can add any metadata fields you need here. These fields are based on 19 | /// and are the metadata fields that 20 | /// Stashh uses for robust NFT display. Urls should be prefixed with `http://`, `https://`, `ipfs://`, or 21 | /// `ar://` 22 | #[derive(Serialize, Deserialize, JsonSchema, Clone, PartialEq, Eq, Debug, Default)] 23 | pub struct Extension { 24 | /// url to the image 25 | pub image: Option, 26 | /// raw SVG image data (not recommended). Only use this if you're not including the image parameter 27 | pub image_data: Option, 28 | /// url to allow users to view the item on your site 29 | pub external_url: Option, 30 | /// item description 31 | pub description: Option, 32 | /// name of the item 33 | pub name: Option, 34 | /// item attributes 35 | pub attributes: Option>, 36 | /// background color represented as a six-character hexadecimal without a pre-pended # 37 | pub background_color: Option, 38 | /// url to a multimedia attachment 39 | pub animation_url: Option, 40 | /// url to a YouTube video 41 | pub youtube_url: Option, 42 | /// media files as specified on Stashh that allows for basic authenticatiion and decryption keys. 43 | /// Most of the above is used for bridging public eth NFT metadata easily, whereas `media` will be used 44 | /// when minting NFTs on Stashh 45 | pub media: Option>, 46 | /// a select list of trait_types that are in the private metadata. This will only ever be used 47 | /// in public metadata 48 | pub protected_attributes: Option>, 49 | } 50 | 51 | /// attribute trait 52 | #[derive(Serialize, Deserialize, JsonSchema, Clone, PartialEq, Eq, Debug, Default)] 53 | pub struct Trait { 54 | /// indicates how a trait should be displayed 55 | pub display_type: Option, 56 | /// name of the trait 57 | pub trait_type: Option, 58 | /// trait value 59 | pub value: String, 60 | /// optional max value for numerical traits 61 | pub max_value: Option, 62 | } 63 | 64 | /// media file 65 | #[derive(Serialize, Deserialize, JsonSchema, Clone, PartialEq, Eq, Debug, Default)] 66 | pub struct MediaFile { 67 | /// file type 68 | /// Stashh currently uses: "image", "video", "audio", "text", "font", "application" 69 | pub file_type: Option, 70 | /// file extension 71 | pub extension: Option, 72 | /// authentication information 73 | pub authentication: Option, 74 | /// url to the file. Urls should be prefixed with `http://`, `https://`, `ipfs://`, or `ar://` 75 | pub url: String, 76 | } 77 | 78 | /// media file authentication 79 | #[derive(Serialize, Deserialize, JsonSchema, Clone, PartialEq, Eq, Debug, Default)] 80 | pub struct Authentication { 81 | /// either a decryption key for encrypted files or a password for basic authentication 82 | pub key: Option, 83 | /// username used in basic authentication 84 | pub user: Option, 85 | } 86 | -------------------------------------------------------------------------------- /packages/storage/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "secret-toolkit-storage" 3 | version = "0.10.3" 4 | edition = "2021" 5 | authors = ["SCRT Labs "] 6 | license-file = "../../LICENSE" 7 | repository = "https://github.com/scrtlabs/secret-toolkit" 8 | readme = "Readme.md" 9 | description = "Abstractions over storage in Secret Contracts" 10 | categories = ["cryptography::cryptocurrencies", "wasm"] 11 | keywords = ["secret-network", "secret-contracts", "secret-toolkit"] 12 | 13 | [package.metadata.docs.rs] 14 | all-features = true 15 | 16 | [dependencies] 17 | serde = { workspace = true } 18 | cosmwasm-std = { workspace = true } 19 | cosmwasm-storage = { workspace = true } 20 | secret-toolkit-serialization = { version = "0.10.3", path = "../serialization" } 21 | -------------------------------------------------------------------------------- /packages/storage/src/item.rs: -------------------------------------------------------------------------------- 1 | use std::any::type_name; 2 | 3 | use std::marker::PhantomData; 4 | 5 | use cosmwasm_std::{StdError, StdResult, Storage}; 6 | use cosmwasm_storage::to_length_prefixed; 7 | 8 | use secret_toolkit_serialization::{Bincode2, Serde}; 9 | use serde::{de::DeserializeOwned, Serialize}; 10 | 11 | /// This storage struct is based on Item from cosmwasm-storage-plus 12 | pub struct Item<'a, T, Ser = Bincode2> 13 | where 14 | T: Serialize + DeserializeOwned, 15 | Ser: Serde, 16 | { 17 | storage_key: &'a [u8], 18 | /// needed if any suffixes were added to the original storage key. 19 | prefix: Option>, 20 | item_type: PhantomData, 21 | serialization_type: PhantomData, 22 | } 23 | 24 | impl<'a, T: Serialize + DeserializeOwned, Ser: Serde> Item<'a, T, Ser> { 25 | /// constructor 26 | pub const fn new(key: &'a [u8]) -> Self { 27 | Self { 28 | storage_key: key, 29 | prefix: None, 30 | item_type: PhantomData, 31 | serialization_type: PhantomData, 32 | } 33 | } 34 | 35 | /// This is used to produce a new Item. This can be used when you want to associate an Item to each user 36 | /// and you still get to define the Item as a static constant 37 | pub fn add_suffix(&self, suffix: &[u8]) -> Self { 38 | let suffix = to_length_prefixed(suffix); 39 | let prefix = self.prefix.as_deref().unwrap_or(self.storage_key); 40 | let prefix = [prefix, suffix.as_slice()].concat(); 41 | Self { 42 | storage_key: self.storage_key, 43 | prefix: Some(prefix), 44 | item_type: self.item_type, 45 | serialization_type: self.serialization_type, 46 | } 47 | } 48 | } 49 | 50 | impl Item<'_, T, Ser> 51 | where 52 | T: Serialize + DeserializeOwned, 53 | Ser: Serde, 54 | { 55 | /// save will serialize the model and store, returns an error on serialization issues 56 | pub fn save(&self, storage: &mut dyn Storage, data: &T) -> StdResult<()> { 57 | self.save_impl(storage, data) 58 | } 59 | 60 | /// userfacing remove function 61 | pub fn remove(&self, storage: &mut dyn Storage) { 62 | self.remove_impl(storage); 63 | } 64 | 65 | /// load will return an error if no data is set at the given key, or on parse error 66 | pub fn load(&self, storage: &dyn Storage) -> StdResult { 67 | self.load_impl(storage) 68 | } 69 | 70 | /// may_load will parse the data stored at the key if present, returns `Ok(None)` if no data there. 71 | /// returns an error on issues parsing 72 | pub fn may_load(&self, storage: &dyn Storage) -> StdResult> { 73 | self.may_load_impl(storage) 74 | } 75 | 76 | /// efficient way to see if any object is currently saved. 77 | pub fn is_empty(&self, storage: &dyn Storage) -> bool { 78 | storage.get(self.as_slice()).is_none() 79 | } 80 | 81 | /// Loads the data, perform the specified action, and store the result 82 | /// in the database. This is shorthand for some common sequences, which may be useful. 83 | /// 84 | /// It assumes, that data was initialized before, and if it doesn't exist, `Err(StdError::NotFound)` 85 | /// is returned. 86 | pub fn update(&self, storage: &mut dyn Storage, action: A) -> StdResult 87 | where 88 | A: FnOnce(T) -> StdResult, 89 | { 90 | let input = self.load_impl(storage)?; 91 | let output = action(input)?; 92 | self.save_impl(storage, &output)?; 93 | Ok(output) 94 | } 95 | 96 | /// Returns StdResult from retrieving the item with the specified key. Returns a 97 | /// StdError::NotFound if there is no item with that key 98 | /// 99 | /// # Arguments 100 | /// 101 | /// * `storage` - a reference to the storage this item is in 102 | fn load_impl(&self, storage: &dyn Storage) -> StdResult { 103 | Ser::deserialize( 104 | &storage 105 | .get(self.as_slice()) 106 | .ok_or_else(|| StdError::not_found(type_name::()))?, 107 | ) 108 | } 109 | 110 | /// Returns StdResult> from retrieving the item with the specified key. Returns a 111 | /// None if there is no item with that key 112 | /// 113 | /// # Arguments 114 | /// 115 | /// * `storage` - a reference to the storage this item is in 116 | fn may_load_impl(&self, storage: &dyn Storage) -> StdResult> { 117 | match storage.get(self.as_slice()) { 118 | Some(value) => Ser::deserialize(&value).map(Some), 119 | None => Ok(None), 120 | } 121 | } 122 | 123 | /// Returns StdResult<()> resulting from saving an item to storage 124 | /// 125 | /// # Arguments 126 | /// 127 | /// * `storage` - a mutable reference to the storage this item should go to 128 | /// * `value` - a reference to the item to store 129 | fn save_impl(&self, storage: &mut dyn Storage, value: &T) -> StdResult<()> { 130 | storage.set(self.as_slice(), &Ser::serialize(value)?); 131 | Ok(()) 132 | } 133 | 134 | /// Removes an item from storage 135 | /// 136 | /// # Arguments 137 | /// 138 | /// * `storage` - a mutable reference to the storage this item is in 139 | fn remove_impl(&self, storage: &mut dyn Storage) { 140 | storage.remove(self.as_slice()); 141 | } 142 | 143 | fn as_slice(&self) -> &[u8] { 144 | if let Some(prefix) = &self.prefix { 145 | prefix 146 | } else { 147 | self.storage_key 148 | } 149 | } 150 | } 151 | 152 | #[cfg(test)] 153 | mod tests { 154 | use cosmwasm_std::testing::MockStorage; 155 | 156 | use secret_toolkit_serialization::Json; 157 | 158 | use super::*; 159 | 160 | #[test] 161 | fn test_item() -> StdResult<()> { 162 | let mut storage = MockStorage::new(); 163 | let item: Item = Item::new(b"test"); 164 | 165 | assert!(item.is_empty(&storage)); 166 | assert_eq!(item.may_load(&storage)?, None); 167 | assert!(item.load(&storage).is_err()); 168 | item.save(&mut storage, &6)?; 169 | assert!(!item.is_empty(&storage)); 170 | assert_eq!(item.load(&storage)?, 6); 171 | assert_eq!(item.may_load(&storage)?, Some(6)); 172 | item.remove(&mut storage); 173 | assert!(item.is_empty(&storage)); 174 | assert_eq!(item.may_load(&storage)?, None); 175 | assert!(item.load(&storage).is_err()); 176 | 177 | Ok(()) 178 | } 179 | 180 | #[test] 181 | fn test_suffix() -> StdResult<()> { 182 | let mut storage = MockStorage::new(); 183 | let item: Item = Item::new(b"test"); 184 | let item1 = item.add_suffix(b"suffix1"); 185 | let item2 = item.add_suffix(b"suffix2"); 186 | 187 | item.save(&mut storage, &0)?; 188 | assert!(item1.is_empty(&storage)); 189 | assert!(item2.is_empty(&storage)); 190 | 191 | item1.save(&mut storage, &1)?; 192 | assert!(!item1.is_empty(&storage)); 193 | assert!(item2.is_empty(&storage)); 194 | assert_eq!(item.may_load(&storage)?, Some(0)); 195 | assert_eq!(item1.may_load(&storage)?, Some(1)); 196 | item2.save(&mut storage, &2)?; 197 | assert_eq!(item.may_load(&storage)?, Some(0)); 198 | assert_eq!(item1.may_load(&storage)?, Some(1)); 199 | assert_eq!(item2.may_load(&storage)?, Some(2)); 200 | 201 | Ok(()) 202 | } 203 | 204 | #[test] 205 | fn test_update() -> StdResult<()> { 206 | let mut storage = MockStorage::new(); 207 | let item: Item = Item::new(b"test"); 208 | 209 | assert!(item.update(&mut storage, |x| Ok(x + 1)).is_err()); 210 | item.save(&mut storage, &7)?; 211 | assert!(item.update(&mut storage, |x| Ok(x + 1)).is_ok()); 212 | assert_eq!(item.load(&storage), Ok(8)); 213 | 214 | Ok(()) 215 | } 216 | 217 | #[test] 218 | fn test_serializations() -> StdResult<()> { 219 | // Check the default behavior is Bincode2 220 | let mut storage = MockStorage::new(); 221 | 222 | let item: Item = Item::new(b"test"); 223 | item.save(&mut storage, &1234)?; 224 | 225 | let key = b"test"; 226 | let bytes = storage.get(key); 227 | assert_eq!(bytes, Some(vec![210, 4, 0, 0])); 228 | 229 | // Check that overriding the serializer with Json works 230 | let mut storage = MockStorage::new(); 231 | let json_item: Item = Item::new(b"test2"); 232 | json_item.save(&mut storage, &1234)?; 233 | 234 | let key = b"test2"; 235 | let bytes = storage.get(key); 236 | assert_eq!(bytes, Some(b"1234".to_vec())); 237 | 238 | Ok(()) 239 | } 240 | } 241 | -------------------------------------------------------------------------------- /packages/storage/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![doc = include_str!("../Readme.md")] 2 | 3 | pub mod append_store; 4 | pub mod deque_store; 5 | pub mod item; 6 | pub mod keymap; 7 | pub mod keyset; 8 | pub mod secure_item; 9 | 10 | pub use append_store::AppendStore; 11 | pub use deque_store::DequeStore; 12 | pub use item::Item; 13 | pub use iter_options::WithoutIter; 14 | use iter_options::{IterOption, WithIter}; 15 | pub use keymap::{Keymap, KeymapBuilder}; 16 | pub use keyset::{Keyset, KeysetBuilder}; 17 | 18 | pub mod iter_options { 19 | pub struct WithIter; 20 | pub struct WithoutIter; 21 | pub trait IterOption {} 22 | 23 | impl IterOption for WithIter {} 24 | impl IterOption for WithoutIter {} 25 | } 26 | -------------------------------------------------------------------------------- /packages/storage/src/secure_item.rs: -------------------------------------------------------------------------------- 1 | use cosmwasm_std::{StdResult, Storage}; 2 | use secret_toolkit_serialization::{Bincode2, Serde}; 3 | use serde::{de::DeserializeOwned, Serialize}; 4 | 5 | use crate::Item; 6 | 7 | pub struct SecureItem<'a, T, Ser = Bincode2> 8 | where 9 | T: Serialize + DeserializeOwned + Copy, 10 | Ser: Serde, 11 | { 12 | item: Item<'a, T, Ser>, 13 | storage: &'a mut dyn Storage, 14 | } 15 | 16 | impl<'a, T: Serialize + DeserializeOwned + Copy, Ser: Serde> SecureItem<'a, T, Ser> { 17 | pub fn new(item: Item<'a, T, Ser>, storage: &'a mut dyn Storage) -> Self { 18 | Self { item, storage } 19 | } 20 | 21 | pub fn add_suffix(&'a mut self, suffix: &[u8]) -> Self { 22 | Self { 23 | item: self.item.add_suffix(suffix), 24 | storage: self.storage, 25 | } 26 | } 27 | } 28 | 29 | impl Drop for SecureItem<'_, T, Ser> 30 | where 31 | T: Serialize + DeserializeOwned + Copy, 32 | Ser: Serde, 33 | { 34 | fn drop(&mut self) { 35 | self.update(|data| Ok(data)).unwrap(); // This is not ideal but can't return `StdResult` 36 | } 37 | } 38 | 39 | impl SecureItem<'_, T, Ser> 40 | where 41 | T: Serialize + DeserializeOwned + Copy, 42 | Ser: Serde, 43 | { 44 | /// save will serialize the model and store, returns an error on serialization issues 45 | pub fn save(&mut self, data: &T) -> StdResult<()> { 46 | self.item.save(self.storage, data) 47 | } 48 | 49 | /// userfacing remove function 50 | pub fn remove(&mut self) { 51 | self.item.remove(self.storage) 52 | } 53 | 54 | /// load will return an error if no data is set at the given key, or on parse error 55 | pub fn load(&self) -> StdResult { 56 | self.item.load(self.storage) 57 | } 58 | 59 | /// may_load will parse the data stored at the key if present, returns `Ok(None)` if no data there. 60 | /// returns an error on issues parsing 61 | pub fn may_load(&self) -> StdResult> { 62 | self.item.may_load(self.storage) 63 | } 64 | 65 | /// efficient way to see if any object is currently saved. 66 | pub fn is_empty(&self) -> bool { 67 | self.item.is_empty(self.storage) 68 | } 69 | 70 | /// Loads the data, perform the specified action, and store the result 71 | /// in the database. This is shorthand for some common sequences, which may be useful. 72 | /// 73 | /// It assumes, that data was initialized before, and if it doesn't exist, `Err(StdError::NotFound)` 74 | /// is returned. 75 | pub fn update(&mut self, action: A) -> StdResult 76 | where 77 | A: FnOnce(T) -> StdResult, 78 | { 79 | self.item.update(self.storage, action) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /packages/utils/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "secret-toolkit-utils" 3 | version = "0.10.3" 4 | edition = "2021" 5 | authors = ["SCRT Labs "] 6 | license-file = "../../LICENSE" 7 | repository = "https://github.com/scrtlabs/secret-toolkit" 8 | readme = "Readme.md" 9 | description = "General utilities for Secret Contracts" 10 | categories = ["cryptography::cryptocurrencies", "wasm"] 11 | keywords = ["secret-network", "secret-contracts", "secret-toolkit"] 12 | 13 | [package.metadata.docs.rs] 14 | all-features = true 15 | 16 | [dependencies] 17 | serde = { workspace = true } 18 | schemars = { workspace = true } 19 | cosmwasm-std = { workspace = true } 20 | cosmwasm-storage = { workspace = true } 21 | -------------------------------------------------------------------------------- /packages/utils/src/calls.rs: -------------------------------------------------------------------------------- 1 | use serde::{de::DeserializeOwned, Serialize}; 2 | 3 | use cosmwasm_std::{ 4 | to_binary, Coin, CosmosMsg, CustomQuery, QuerierWrapper, QueryRequest, StdResult, Uint128, 5 | WasmMsg, WasmQuery, 6 | }; 7 | 8 | use super::space_pad; 9 | 10 | /// A trait marking types that define the instantiation message of a contract 11 | /// 12 | /// This trait requires specifying a padding block size and provides a method to create the 13 | /// CosmosMsg used to instantiate a contract 14 | pub trait InitCallback: Serialize { 15 | /// pad the message to blocks of this size 16 | const BLOCK_SIZE: usize; 17 | 18 | /// Returns StdResult 19 | /// 20 | /// Tries to convert the instance of the implementing type to a CosmosMsg that will trigger the 21 | /// instantiation of a contract. The BLOCK_SIZE specified in the implementation is used when 22 | /// padding the message 23 | /// 24 | /// # Arguments 25 | /// 26 | /// * `label` - String holding the label for the new contract instance 27 | /// * `code_id` - code ID of the contract to be instantiated 28 | /// * `code_hash` - String holding the code hash of the contract to be instantiated 29 | /// * `funds_amount` - Optional Uint128 amount of native coin to send with instantiation message 30 | fn to_cosmos_msg( 31 | &self, 32 | admin: Option, 33 | label: String, 34 | code_id: u64, 35 | code_hash: String, 36 | funds_amount: Option, 37 | ) -> StdResult { 38 | let mut msg = to_binary(self)?; 39 | // can not have 0 block size 40 | let padding = if Self::BLOCK_SIZE == 0 { 41 | 1 42 | } else { 43 | Self::BLOCK_SIZE 44 | }; 45 | space_pad(&mut msg.0, padding); 46 | let mut funds = Vec::new(); 47 | if let Some(amount) = funds_amount { 48 | funds.push(Coin { 49 | amount, 50 | denom: String::from("uscrt"), 51 | }); 52 | } 53 | let init = WasmMsg::Instantiate { 54 | admin, 55 | code_id, 56 | msg, 57 | code_hash, 58 | funds, 59 | label, 60 | }; 61 | Ok(init.into()) 62 | } 63 | } 64 | 65 | /// A trait marking types that define the handle message(s) of a contract 66 | /// 67 | /// This trait requires specifying a padding block size and provides a method to create the 68 | /// CosmosMsg used to execute a handle method of a contract 69 | pub trait HandleCallback: Serialize { 70 | /// pad the message to blocks of this size 71 | const BLOCK_SIZE: usize; 72 | 73 | /// Returns StdResult 74 | /// 75 | /// Tries to convert the instance of the implementing type to a CosmosMsg that will trigger a 76 | /// handle function of a contract. The BLOCK_SIZE specified in the implementation is used when 77 | /// padding the message 78 | /// 79 | /// # Arguments 80 | /// 81 | /// * `code_hash` - String holding the code hash of the contract to be executed 82 | /// * `contract_addr` - address of the contract being called 83 | /// * `funds_amount` - Optional Uint128 amount of native coin to send with the handle message 84 | fn to_cosmos_msg( 85 | &self, 86 | code_hash: String, 87 | contract_addr: String, 88 | funds_amount: Option, 89 | ) -> StdResult { 90 | let mut msg = to_binary(self)?; 91 | // can not have 0 block size 92 | let padding = if Self::BLOCK_SIZE == 0 { 93 | 1 94 | } else { 95 | Self::BLOCK_SIZE 96 | }; 97 | space_pad(&mut msg.0, padding); 98 | let mut funds = Vec::new(); 99 | if let Some(amount) = funds_amount { 100 | funds.push(Coin { 101 | amount, 102 | denom: String::from("uscrt"), 103 | }); 104 | } 105 | let execute = WasmMsg::Execute { 106 | msg, 107 | contract_addr, 108 | code_hash, 109 | funds, 110 | }; 111 | Ok(execute.into()) 112 | } 113 | } 114 | 115 | /// A trait marking types that define the query message(s) of a contract 116 | /// 117 | /// This trait requires specifying a padding block size and provides a method to query a contract 118 | pub trait Query: Serialize { 119 | /// pad the message to blocks of this size 120 | const BLOCK_SIZE: usize; 121 | 122 | /// Returns StdResult, where T is the type defining the query response 123 | /// 124 | /// Tries to query a contract and deserialize the query response. The BLOCK_SIZE specified in the 125 | /// implementation is used when padding the message 126 | /// 127 | /// # Arguments 128 | /// 129 | /// * `querier` - a reference to the Querier dependency of the querying contract 130 | /// * `callback_code_hash` - String holding the code hash of the contract to be queried 131 | /// * `contract_addr` - address of the contract being queried 132 | fn query( 133 | &self, 134 | querier: QuerierWrapper, 135 | code_hash: String, 136 | contract_addr: String, 137 | ) -> StdResult { 138 | let mut msg = to_binary(self)?; 139 | // can not have 0 block size 140 | let padding = if Self::BLOCK_SIZE == 0 { 141 | 1 142 | } else { 143 | Self::BLOCK_SIZE 144 | }; 145 | space_pad(&mut msg.0, padding); 146 | querier.query(&QueryRequest::Wasm(WasmQuery::Smart { 147 | contract_addr, 148 | code_hash, 149 | msg, 150 | })) 151 | } 152 | } 153 | 154 | #[cfg(test)] 155 | mod tests { 156 | use super::*; 157 | use cosmwasm_std::{ 158 | to_vec, Binary, ContractResult, Empty, Querier, QuerierResult, SystemError, SystemResult, 159 | }; 160 | use serde::Deserialize; 161 | 162 | #[derive(Serialize)] 163 | struct FooInit { 164 | pub f1: i8, 165 | pub f2: i8, 166 | } 167 | 168 | impl InitCallback for FooInit { 169 | const BLOCK_SIZE: usize = 256; 170 | } 171 | 172 | #[derive(Serialize)] 173 | enum FooHandle { 174 | Var1 { f1: i8, f2: i8 }, 175 | } 176 | 177 | // All you really need to do is make people give you the padding block size. 178 | impl HandleCallback for FooHandle { 179 | const BLOCK_SIZE: usize = 256; 180 | } 181 | 182 | #[derive(Serialize)] 183 | enum FooQuery { 184 | Query1 { f1: i8, f2: i8 }, 185 | } 186 | 187 | impl Query for FooQuery { 188 | const BLOCK_SIZE: usize = 256; 189 | } 190 | 191 | #[test] 192 | fn test_handle_callback_implementation_works() -> StdResult<()> { 193 | let address = "secret1xyzasdf".to_string(); 194 | let hash = "asdf".to_string(); 195 | let amount = Uint128::new(1234); 196 | 197 | let cosmos_message: CosmosMsg = FooHandle::Var1 { f1: 1, f2: 2 }.to_cosmos_msg( 198 | hash.clone(), 199 | address.clone(), 200 | Some(amount), 201 | )?; 202 | 203 | match cosmos_message { 204 | CosmosMsg::Wasm(WasmMsg::Execute { 205 | contract_addr, 206 | code_hash, 207 | msg, 208 | funds, 209 | }) => { 210 | assert_eq!(contract_addr, address); 211 | assert_eq!(code_hash, hash); 212 | let mut expected_msg = r#"{"Var1":{"f1":1,"f2":2}}"#.as_bytes().to_vec(); 213 | space_pad(&mut expected_msg, 256); 214 | assert_eq!(msg.0, expected_msg); 215 | assert_eq!(funds, vec![Coin::new(amount.u128(), "uscrt")]) 216 | } 217 | other => panic!("unexpected CosmosMsg variant: {:?}", other), 218 | }; 219 | 220 | Ok(()) 221 | } 222 | 223 | #[test] 224 | fn test_init_callback_implementation_works() -> StdResult<()> { 225 | let adm = "addr1".to_string(); 226 | let lbl = "testlabel".to_string(); 227 | let id = 17u64; 228 | let hash = "asdf".to_string(); 229 | let amount = Uint128::new(1234); 230 | 231 | let cosmos_message: CosmosMsg = FooInit { f1: 1, f2: 2 }.to_cosmos_msg( 232 | Some(adm.clone()), 233 | lbl.clone(), 234 | id, 235 | hash.clone(), 236 | Some(amount), 237 | )?; 238 | 239 | match cosmos_message { 240 | CosmosMsg::Wasm(WasmMsg::Instantiate { 241 | admin, 242 | code_id, 243 | msg, 244 | code_hash, 245 | funds, 246 | label, 247 | }) => { 248 | assert_eq!(admin, Some(adm)); 249 | assert_eq!(code_id, id); 250 | let mut expected_msg = r#"{"f1":1,"f2":2}"#.as_bytes().to_vec(); 251 | space_pad(&mut expected_msg, 256); 252 | assert_eq!(msg.0, expected_msg); 253 | assert_eq!(code_hash, hash); 254 | assert_eq!(funds, vec![Coin::new(amount.u128(), "uscrt")]); 255 | assert_eq!(label, lbl) 256 | } 257 | other => panic!("unexpected CosmosMsg variant: {:?}", other), 258 | }; 259 | 260 | Ok(()) 261 | } 262 | 263 | #[test] 264 | fn test_query_works() -> StdResult<()> { 265 | #[derive(Serialize, Deserialize, PartialEq, Debug)] 266 | struct QueryResponse { 267 | bar1: i8, 268 | bar2: i8, 269 | } 270 | 271 | struct MyMockQuerier {} 272 | 273 | impl Querier for MyMockQuerier { 274 | fn raw_query(&self, request: &[u8]) -> QuerierResult { 275 | let mut expected_msg = r#"{"Query1":{"f1":1,"f2":2}}"#.as_bytes().to_vec(); 276 | space_pad(&mut expected_msg, 256); 277 | let expected_request: QueryRequest = 278 | QueryRequest::Wasm(WasmQuery::Smart { 279 | contract_addr: "secret1xyzasdf".to_string(), 280 | code_hash: "asdf".to_string(), 281 | msg: Binary(expected_msg), 282 | }); 283 | let test_req: &[u8] = &to_vec(&expected_request).unwrap(); 284 | assert_eq!(request, test_req); 285 | let response = match to_binary(&QueryResponse { bar1: 1, bar2: 2 }) { 286 | Ok(response) => ContractResult::Ok(response), 287 | Err(_e) => return SystemResult::Err(SystemError::Unknown {}), 288 | }; 289 | SystemResult::Ok(response) 290 | } 291 | } 292 | 293 | let querier = QuerierWrapper::::new(&MyMockQuerier {}); 294 | let address = "secret1xyzasdf".to_string(); 295 | let hash = "asdf".to_string(); 296 | 297 | let response: QueryResponse = 298 | FooQuery::Query1 { f1: 1, f2: 2 }.query(querier, hash, address)?; 299 | assert_eq!(response, QueryResponse { bar1: 1, bar2: 2 }); 300 | 301 | Ok(()) 302 | } 303 | } 304 | -------------------------------------------------------------------------------- /packages/utils/src/feature_toggle.rs: -------------------------------------------------------------------------------- 1 | use cosmwasm_std::{ 2 | to_binary, to_vec, Addr, Binary, Deps, DepsMut, MessageInfo, Response, StdError, StdResult, 3 | Storage, 4 | }; 5 | use cosmwasm_storage::{Bucket, ReadonlyBucket}; 6 | use schemars::JsonSchema; 7 | use serde::de::DeserializeOwned; 8 | use serde::{Deserialize, Serialize}; 9 | 10 | const PREFIX_FEATURES: &[u8] = b"features"; 11 | const PREFIX_PAUSERS: &[u8] = b"pausers"; 12 | 13 | pub struct FeatureToggle; 14 | 15 | impl FeatureToggleTrait for FeatureToggle { 16 | const STORAGE_KEY: &'static [u8] = b"feature_toggle"; 17 | } 18 | 19 | pub trait FeatureToggleTrait { 20 | const STORAGE_KEY: &'static [u8]; 21 | 22 | fn init_features( 23 | storage: &mut dyn Storage, 24 | feature_statuses: Vec>, 25 | pausers: Vec, 26 | ) -> StdResult<()> { 27 | for fs in feature_statuses { 28 | Self::set_feature_status(storage, &fs.feature, fs.status)?; 29 | } 30 | 31 | for p in pausers { 32 | Self::set_pauser(storage, &p)?; 33 | } 34 | 35 | Ok(()) 36 | } 37 | 38 | fn require_not_paused(storage: &dyn Storage, features: Vec) -> StdResult<()> { 39 | for feature in features { 40 | let status = Self::get_feature_status(storage, &feature)?; 41 | match status { 42 | None => { 43 | return Err(StdError::generic_err(format!( 44 | "feature toggle: unknown feature '{}'", 45 | String::from_utf8_lossy(&to_vec(&feature)?) 46 | ))) 47 | } 48 | Some(s) => match s { 49 | Status::NotPaused => {} 50 | Status::Paused => { 51 | return Err(StdError::generic_err(format!( 52 | "feature toggle: feature '{}' is paused", 53 | String::from_utf8_lossy(&to_vec(&feature)?) 54 | ))); 55 | } 56 | }, 57 | } 58 | } 59 | 60 | Ok(()) 61 | } 62 | 63 | fn pause(storage: &mut dyn Storage, features: Vec) -> StdResult<()> { 64 | for f in features { 65 | Self::set_feature_status(storage, &f, Status::Paused)?; 66 | } 67 | 68 | Ok(()) 69 | } 70 | 71 | fn unpause(storage: &mut dyn Storage, features: Vec) -> StdResult<()> { 72 | for f in features { 73 | Self::set_feature_status(storage, &f, Status::NotPaused)?; 74 | } 75 | 76 | Ok(()) 77 | } 78 | 79 | fn is_pauser(storage: &dyn Storage, key: &Addr) -> StdResult { 80 | let feature_store: ReadonlyBucket = 81 | ReadonlyBucket::multilevel(storage, &[Self::STORAGE_KEY, PREFIX_PAUSERS]); 82 | feature_store.may_load(key.as_bytes()).map(|p| p.is_some()) 83 | } 84 | 85 | fn set_pauser(storage: &mut dyn Storage, key: &Addr) -> StdResult<()> { 86 | let mut feature_store = Bucket::multilevel(storage, &[Self::STORAGE_KEY, PREFIX_PAUSERS]); 87 | feature_store.save(key.as_bytes(), &true /* value is insignificant */) 88 | } 89 | 90 | fn remove_pauser(storage: &mut dyn Storage, key: &Addr) { 91 | let mut feature_store: Bucket = 92 | Bucket::multilevel(storage, &[Self::STORAGE_KEY, PREFIX_PAUSERS]); 93 | feature_store.remove(key.as_bytes()) 94 | } 95 | 96 | fn get_feature_status( 97 | storage: &dyn Storage, 98 | key: &T, 99 | ) -> StdResult> { 100 | let feature_store = 101 | ReadonlyBucket::multilevel(storage, &[Self::STORAGE_KEY, PREFIX_FEATURES]); 102 | feature_store.may_load(&cosmwasm_std::to_vec(&key)?) 103 | } 104 | 105 | fn set_feature_status( 106 | storage: &mut dyn Storage, 107 | key: &T, 108 | item: Status, 109 | ) -> StdResult<()> { 110 | let mut feature_store = Bucket::multilevel(storage, &[Self::STORAGE_KEY, PREFIX_FEATURES]); 111 | feature_store.save(&cosmwasm_std::to_vec(&key)?, &item) 112 | } 113 | 114 | fn handle_pause( 115 | deps: DepsMut, 116 | info: &MessageInfo, 117 | features: Vec, 118 | ) -> StdResult { 119 | if !Self::is_pauser(deps.storage, &info.sender)? { 120 | return Err(StdError::generic_err("unauthorized")); 121 | } 122 | 123 | Self::pause(deps.storage, features)?; 124 | 125 | Ok(Response::new().set_data(to_binary(&HandleAnswer::Pause { 126 | status: ResponseStatus::Success, 127 | })?)) 128 | } 129 | 130 | fn handle_unpause( 131 | deps: DepsMut, 132 | info: &MessageInfo, 133 | features: Vec, 134 | ) -> StdResult { 135 | if !Self::is_pauser(deps.storage, &info.sender)? { 136 | return Err(StdError::generic_err("unauthorized")); 137 | } 138 | 139 | Self::unpause(deps.storage, features)?; 140 | 141 | Ok(Response::new().set_data(to_binary(&HandleAnswer::Unpause { 142 | status: ResponseStatus::Success, 143 | })?)) 144 | } 145 | 146 | fn handle_set_pauser(deps: DepsMut, address: Addr) -> StdResult { 147 | Self::set_pauser(deps.storage, &address)?; 148 | 149 | Ok( 150 | Response::new().set_data(to_binary(&HandleAnswer::SetPauser { 151 | status: ResponseStatus::Success, 152 | })?), 153 | ) 154 | } 155 | 156 | fn handle_remove_pauser(deps: DepsMut, address: Addr) -> StdResult { 157 | Self::remove_pauser(deps.storage, &address); 158 | 159 | Ok( 160 | Response::new().set_data(to_binary(&HandleAnswer::RemovePauser { 161 | status: ResponseStatus::Success, 162 | })?), 163 | ) 164 | } 165 | 166 | fn query_status(deps: Deps, features: Vec) -> StdResult { 167 | let mut status = Vec::with_capacity(features.len()); 168 | for feature in features { 169 | match Self::get_feature_status(deps.storage, &feature)? { 170 | None => { 171 | return Err(StdError::generic_err(format!( 172 | "invalid feature: {} does not exist", 173 | String::from_utf8_lossy(&to_vec(&feature)?) 174 | ))) 175 | } 176 | Some(s) => status.push(FeatureStatus { feature, status: s }), 177 | } 178 | } 179 | 180 | to_binary(&FeatureToggleQueryAnswer::Status { features: status }) 181 | } 182 | 183 | fn query_is_pauser(deps: Deps, address: Addr) -> StdResult { 184 | let is_pauser = Self::is_pauser(deps.storage, &address)?; 185 | 186 | to_binary(&FeatureToggleQueryAnswer::<()>::IsPauser { is_pauser }) 187 | } 188 | } 189 | 190 | #[derive(Serialize, Debug, Deserialize, Clone, JsonSchema, PartialEq, Eq, Default)] 191 | pub enum Status { 192 | #[default] 193 | NotPaused, 194 | Paused, 195 | } 196 | 197 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] 198 | #[serde(rename_all = "snake_case")] 199 | pub enum FeatureToggleHandleMsg { 200 | #[serde(bound = "")] 201 | Pause { 202 | features: Vec, 203 | }, 204 | #[serde(bound = "")] 205 | Unpause { 206 | features: Vec, 207 | }, 208 | SetPauser { 209 | address: String, 210 | }, 211 | RemovePauser { 212 | address: String, 213 | }, 214 | } 215 | 216 | #[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] 217 | #[serde(rename_all = "snake_case")] 218 | enum ResponseStatus { 219 | Success, 220 | Failure, 221 | } 222 | 223 | #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] 224 | #[serde(rename_all = "snake_case")] 225 | enum HandleAnswer { 226 | Pause { status: ResponseStatus }, 227 | Unpause { status: ResponseStatus }, 228 | SetPauser { status: ResponseStatus }, 229 | RemovePauser { status: ResponseStatus }, 230 | } 231 | 232 | #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] 233 | #[serde(rename_all = "snake_case")] 234 | pub enum FeatureToggleQueryMsg { 235 | #[serde(bound = "")] 236 | Status { 237 | features: Vec, 238 | }, 239 | IsPauser { 240 | address: String, 241 | }, 242 | } 243 | 244 | #[derive(Serialize, Deserialize, JsonSchema, Debug)] 245 | #[serde(rename_all = "snake_case")] 246 | enum FeatureToggleQueryAnswer { 247 | Status { features: Vec> }, 248 | IsPauser { is_pauser: bool }, 249 | } 250 | 251 | #[derive(Serialize, Deserialize, JsonSchema, Debug)] 252 | pub struct FeatureStatus { 253 | pub feature: T, 254 | pub status: Status, 255 | } 256 | 257 | #[cfg(test)] 258 | mod tests { 259 | use crate::feature_toggle::{ 260 | FeatureStatus, FeatureToggle, FeatureToggleHandleMsg, FeatureToggleQueryMsg, 261 | FeatureToggleTrait, HandleAnswer, ResponseStatus, Status, 262 | }; 263 | use cosmwasm_std::testing::{mock_dependencies, mock_info, MockStorage}; 264 | use cosmwasm_std::{from_binary, Addr, MemoryStorage, StdError, StdResult}; 265 | 266 | fn init_features(storage: &mut MemoryStorage) -> StdResult<()> { 267 | FeatureToggle::init_features( 268 | storage, 269 | vec![ 270 | FeatureStatus { 271 | feature: "Feature1".to_string(), 272 | status: Status::NotPaused, 273 | }, 274 | FeatureStatus { 275 | feature: "Feature2".to_string(), 276 | status: Status::NotPaused, 277 | }, 278 | FeatureStatus { 279 | feature: "Feature3".to_string(), 280 | status: Status::Paused, 281 | }, 282 | ], 283 | vec![Addr::unchecked("alice".to_string())], 284 | ) 285 | } 286 | 287 | #[test] 288 | fn test_init_works() -> StdResult<()> { 289 | let mut storage = MockStorage::new(); 290 | init_features(&mut storage)?; 291 | 292 | assert_eq!( 293 | FeatureToggle::get_feature_status(&storage, &"Feature1".to_string())?, 294 | Some(Status::NotPaused) 295 | ); 296 | assert_eq!( 297 | FeatureToggle::get_feature_status(&storage, &"Feature2".to_string())?, 298 | Some(Status::NotPaused) 299 | ); 300 | assert_eq!( 301 | FeatureToggle::get_feature_status(&storage, &"Feature3".to_string())?, 302 | Some(Status::Paused) 303 | ); 304 | assert_eq!( 305 | FeatureToggle::get_feature_status(&storage, &"Feature4".to_string())?, 306 | None 307 | ); 308 | 309 | assert!(FeatureToggle::is_pauser( 310 | &storage, 311 | &Addr::unchecked("alice".to_string()) 312 | )?,); 313 | assert!(!FeatureToggle::is_pauser( 314 | &storage, 315 | &Addr::unchecked("bob".to_string()) 316 | )?); 317 | 318 | Ok(()) 319 | } 320 | 321 | #[test] 322 | fn test_unpause() -> StdResult<()> { 323 | let mut storage = MockStorage::new(); 324 | init_features(&mut storage)?; 325 | 326 | FeatureToggle::unpause(&mut storage, vec!["Feature3".to_string()])?; 327 | assert_eq!( 328 | FeatureToggle::get_feature_status(&storage, &"Feature3".to_string())?, 329 | Some(Status::NotPaused) 330 | ); 331 | 332 | Ok(()) 333 | } 334 | 335 | #[test] 336 | fn test_handle_unpause() -> StdResult<()> { 337 | let mut deps = mock_dependencies(); 338 | init_features(&mut deps.storage)?; 339 | 340 | let info = mock_info("non-pauser", &[]); 341 | let error = 342 | FeatureToggle::handle_unpause(deps.as_mut(), &info, vec!["Feature3".to_string()]); 343 | assert_eq!(error, Err(StdError::generic_err("unauthorized"))); 344 | 345 | let info = mock_info("alice", &[]); 346 | let response = 347 | FeatureToggle::handle_unpause(deps.as_mut(), &info, vec!["Feature3".to_string()])?; 348 | let answer: HandleAnswer = from_binary(&response.data.unwrap())?; 349 | 350 | assert_eq!( 351 | answer, 352 | HandleAnswer::Unpause { 353 | status: ResponseStatus::Success, 354 | } 355 | ); 356 | Ok(()) 357 | } 358 | 359 | #[test] 360 | fn test_pause() -> StdResult<()> { 361 | let mut storage = MockStorage::new(); 362 | init_features(&mut storage)?; 363 | 364 | FeatureToggle::pause(&mut storage, vec!["Feature1".to_string()])?; 365 | assert_eq!( 366 | FeatureToggle::get_feature_status(&storage, &"Feature1".to_string())?, 367 | Some(Status::Paused) 368 | ); 369 | 370 | Ok(()) 371 | } 372 | 373 | #[test] 374 | fn test_handle_pause() -> StdResult<()> { 375 | let mut deps = mock_dependencies(); 376 | init_features(&mut deps.storage)?; 377 | 378 | let info = mock_info("non-pauser", &[]); 379 | let error = FeatureToggle::handle_pause(deps.as_mut(), &info, vec!["Feature2".to_string()]); 380 | assert_eq!(error, Err(StdError::generic_err("unauthorized"))); 381 | 382 | let info = mock_info("alice", &[]); 383 | let response = 384 | FeatureToggle::handle_pause(deps.as_mut(), &info, vec!["Feature2".to_string()])?; 385 | let answer: HandleAnswer = from_binary(&response.data.unwrap())?; 386 | 387 | assert_eq!( 388 | answer, 389 | HandleAnswer::Pause { 390 | status: ResponseStatus::Success, 391 | } 392 | ); 393 | Ok(()) 394 | } 395 | 396 | #[test] 397 | fn test_require_not_paused() -> StdResult<()> { 398 | let mut storage = MockStorage::new(); 399 | init_features(&mut storage)?; 400 | 401 | assert!( 402 | FeatureToggle::require_not_paused(&storage, vec!["Feature1".to_string()]).is_ok(), 403 | "{:?}", 404 | FeatureToggle::require_not_paused(&storage, vec!["Feature1".to_string()]) 405 | ); 406 | assert!( 407 | FeatureToggle::require_not_paused(&storage, vec!["Feature3".to_string()]).is_err(), 408 | "{:?}", 409 | FeatureToggle::require_not_paused(&storage, vec!["Feature3".to_string()]) 410 | ); 411 | 412 | Ok(()) 413 | } 414 | 415 | #[test] 416 | fn test_add_remove_pausers() -> StdResult<()> { 417 | let mut storage = MockStorage::new(); 418 | init_features(&mut storage)?; 419 | 420 | let bob = Addr::unchecked("bob".to_string()); 421 | 422 | FeatureToggle::set_pauser(&mut storage, &bob)?; 423 | assert!( 424 | FeatureToggle::is_pauser(&storage, &bob)?, 425 | "{:?}", 426 | FeatureToggle::is_pauser(&storage, &bob) 427 | ); 428 | 429 | FeatureToggle::remove_pauser(&mut storage, &bob); 430 | assert!( 431 | !FeatureToggle::is_pauser(&storage, &bob)?, 432 | "{:?}", 433 | FeatureToggle::is_pauser(&storage, &bob) 434 | ); 435 | 436 | Ok(()) 437 | } 438 | 439 | #[test] 440 | fn test_deserialize_messages() { 441 | use serde::{Deserialize, Serialize}; 442 | 443 | #[derive(Serialize, Deserialize, Debug, PartialEq)] 444 | #[serde(rename_all = "snake_case")] 445 | enum Features { 446 | Var1, 447 | Var2, 448 | } 449 | 450 | let handle_msg = b"{\"pause\":{\"features\":[\"var1\",\"var2\"]}}"; 451 | let query_msg = b"{\"status\":{\"features\": [\"var1\"]}}"; 452 | let query_msg_invalid = b"{\"status\":{\"features\": [\"var3\"]}}"; 453 | 454 | let parsed: FeatureToggleHandleMsg = 455 | cosmwasm_std::from_slice(handle_msg).unwrap(); 456 | assert_eq!( 457 | parsed, 458 | FeatureToggleHandleMsg::Pause { 459 | features: vec![Features::Var1, Features::Var2] 460 | } 461 | ); 462 | let parsed: FeatureToggleQueryMsg = cosmwasm_std::from_slice(query_msg).unwrap(); 463 | assert_eq!( 464 | parsed, 465 | FeatureToggleQueryMsg::Status { 466 | features: vec![Features::Var1] 467 | } 468 | ); 469 | let parsed: StdResult> = 470 | cosmwasm_std::from_slice(query_msg_invalid); 471 | assert!(parsed.is_err()); 472 | } 473 | } 474 | -------------------------------------------------------------------------------- /packages/utils/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![doc = include_str!("../Readme.md")] 2 | 3 | pub mod calls; 4 | pub mod feature_toggle; 5 | pub mod padding; 6 | pub mod types; 7 | 8 | pub use calls::*; 9 | pub use padding::*; 10 | -------------------------------------------------------------------------------- /packages/utils/src/padding.rs: -------------------------------------------------------------------------------- 1 | use cosmwasm_std::{Binary, Response}; 2 | 3 | /// Take a Vec and pad it up to a multiple of `block_size`, using spaces at the end. 4 | pub fn space_pad(message: &mut Vec, block_size: usize) -> &mut Vec { 5 | let len = message.len(); 6 | let surplus = len % block_size; 7 | if surplus == 0 { 8 | return message; 9 | } 10 | 11 | let missing = block_size - surplus; 12 | message.reserve(missing); 13 | message.extend(std::iter::repeat(b' ').take(missing)); 14 | message 15 | } 16 | 17 | /// Pad the data and logs in a `Result` to the block size, with spaces. 18 | // Users don't need to care about it as the type `T` has a default, and will 19 | // always be known in the context of the caller. 20 | pub fn pad_handle_result( 21 | response: Result, E>, 22 | block_size: usize, 23 | ) -> Result, E> 24 | where 25 | T: Clone + std::fmt::Debug + PartialEq + schemars::JsonSchema, 26 | { 27 | response.map(|mut response| { 28 | response.data = response.data.map(|mut data| { 29 | space_pad(&mut data.0, block_size); 30 | data 31 | }); 32 | for attribute in &mut response.attributes { 33 | // do not pad plaintext attributes 34 | if attribute.encrypted { 35 | // Safety: These two are safe because we know the characters that 36 | // `space_pad` appends are valid UTF-8 37 | unsafe { space_pad(attribute.key.as_mut_vec(), block_size) }; 38 | unsafe { space_pad(attribute.value.as_mut_vec(), block_size) }; 39 | } 40 | } 41 | response 42 | }) 43 | } 44 | 45 | /// Pad a `QueryResult` with spaces 46 | pub fn pad_query_result(response: Result, block_size: usize) -> Result { 47 | response.map(|mut response| { 48 | space_pad(&mut response.0, block_size); 49 | response 50 | }) 51 | } 52 | -------------------------------------------------------------------------------- /packages/utils/src/types.rs: -------------------------------------------------------------------------------- 1 | use schemars::JsonSchema; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | #[derive(Serialize, Deserialize, Eq, PartialEq, Debug, Clone, JsonSchema)] 5 | pub struct Contract { 6 | pub address: String, 7 | pub hash: String, 8 | } 9 | 10 | #[derive(Serialize, Deserialize, Eq, PartialEq, Debug, Clone, JsonSchema)] 11 | pub struct WasmCode { 12 | pub code_id: u64, 13 | pub hash: String, 14 | } 15 | 16 | #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] 17 | #[serde(rename_all = "snake_case")] 18 | pub enum Token { 19 | Snip20(Contract), 20 | Native(String), 21 | } 22 | -------------------------------------------------------------------------------- /packages/viewing_key/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "secret-toolkit-viewing-key" 3 | version = "0.10.3" 4 | edition = "2021" 5 | authors = ["SCRT Labs "] 6 | license-file = "../../LICENSE" 7 | repository = "https://github.com/scrtlabs/secret-toolkit" 8 | readme = "Readme.md" 9 | description = "Boilerplate for using viewing keys in Secret Contracts" 10 | categories = ["cryptography::cryptocurrencies", "wasm"] 11 | keywords = ["secret-network", "secret-contracts", "secret-toolkit"] 12 | 13 | [package.metadata.docs.rs] 14 | all-features = true 15 | 16 | [dependencies] 17 | serde = { workspace = true } 18 | schemars = { workspace = true } 19 | base64 = "0.21.0" 20 | subtle = { version = "2.2.3", default-features = false } 21 | cosmwasm-std = { workspace = true } 22 | cosmwasm-storage = { workspace = true } 23 | secret-toolkit-crypto = { version = "0.10.3", path = "../crypto", features = [ 24 | "hash", 25 | "rand", 26 | ] } 27 | secret-toolkit-utils = { version = "0.10.3", path = "../utils" } 28 | -------------------------------------------------------------------------------- /packages/viewing_key/Readme.md: -------------------------------------------------------------------------------- 1 | # Secret Contract Development Toolkit - Permits 2 | 3 | ⚠️ This package is a sub-package of the `secret-toolkit` package. Please see its crate page for more context. 4 | 5 | Utils for implementing permits, used by SNIP20 & SNIP721. 6 | -------------------------------------------------------------------------------- /packages/viewing_key/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![doc = include_str!("../Readme.md")] 2 | 3 | extern crate core; 4 | 5 | use base64::{engine::general_purpose, Engine as _}; 6 | use subtle::ConstantTimeEq; 7 | 8 | use cosmwasm_std::{Env, MessageInfo, StdError, StdResult, Storage}; 9 | use cosmwasm_storage::{PrefixedStorage, ReadonlyPrefixedStorage}; 10 | 11 | use secret_toolkit_crypto::{sha_256, ContractPrng, SHA256_HASH_SIZE}; 12 | 13 | pub const VIEWING_KEY_SIZE: usize = SHA256_HASH_SIZE; 14 | pub const VIEWING_KEY_PREFIX: &str = "api_key_"; 15 | const SEED_KEY: &[u8] = b"::seed"; 16 | 17 | /// This is the default implementation of the viewing key store, using the "viewing_keys" 18 | /// storage prefix. 19 | /// 20 | /// You can use another storage location by implementing `ViewingKeyStore` for your own type. 21 | pub struct ViewingKey; 22 | 23 | impl ViewingKeyStore for ViewingKey { 24 | const STORAGE_KEY: &'static [u8] = b"viewing_keys"; 25 | } 26 | 27 | /// A trait describing the interface of a Viewing Key store/vault. 28 | /// 29 | /// It includes a default implementation that only requires specifying where in the storage 30 | /// the keys should be held. 31 | pub trait ViewingKeyStore { 32 | const STORAGE_KEY: &'static [u8]; 33 | 34 | /// Set the initial prng seed for the store 35 | fn set_seed(storage: &mut dyn Storage, seed: &[u8]) { 36 | let mut seed_key = Vec::new(); 37 | seed_key.extend_from_slice(Self::STORAGE_KEY); 38 | seed_key.extend_from_slice(SEED_KEY); 39 | 40 | storage.set(&seed_key, seed) 41 | } 42 | 43 | /// Create a new viewing key, save it to storage, and return it. 44 | /// 45 | /// The random entropy should be provided from some external source, such as the user. 46 | fn create( 47 | storage: &mut dyn Storage, 48 | info: &MessageInfo, 49 | env: &Env, 50 | account: &str, 51 | entropy: &[u8], 52 | ) -> String { 53 | let mut seed_key = Vec::with_capacity(Self::STORAGE_KEY.len() + SEED_KEY.len()); 54 | seed_key.extend_from_slice(Self::STORAGE_KEY); 55 | seed_key.extend_from_slice(SEED_KEY); 56 | let seed = storage.get(&seed_key).unwrap_or_default(); 57 | 58 | let (viewing_key, next_seed) = new_viewing_key(info, env, &seed, entropy); 59 | let mut balance_store = PrefixedStorage::new(storage, Self::STORAGE_KEY); 60 | let hashed_key = sha_256(viewing_key.as_bytes()); 61 | balance_store.set(account.as_bytes(), &hashed_key); 62 | 63 | storage.set(&seed_key, &next_seed); 64 | 65 | viewing_key 66 | } 67 | 68 | /// Set a new viewing key based on a predetermined value. 69 | fn set(storage: &mut dyn Storage, account: &str, viewing_key: &str) { 70 | let mut balance_store = PrefixedStorage::new(storage, Self::STORAGE_KEY); 71 | balance_store.set(account.as_bytes(), &sha_256(viewing_key.as_bytes())); 72 | } 73 | 74 | /// Check if a viewing key matches an account. 75 | fn check(storage: &dyn Storage, account: &str, viewing_key: &str) -> StdResult<()> { 76 | let balance_store = ReadonlyPrefixedStorage::new(storage, Self::STORAGE_KEY); 77 | let expected_hash = balance_store.get(account.as_bytes()); 78 | let expected_hash = match &expected_hash { 79 | Some(hash) => hash.as_slice(), 80 | None => &[0u8; VIEWING_KEY_SIZE], 81 | }; 82 | let key_hash = sha_256(viewing_key.as_bytes()); 83 | if ct_slice_compare(&key_hash, expected_hash) { 84 | Ok(()) 85 | } else { 86 | Err(StdError::generic_err("unauthorized")) 87 | } 88 | } 89 | } 90 | 91 | fn new_viewing_key( 92 | info: &MessageInfo, 93 | env: &Env, 94 | seed: &[u8], 95 | entropy: &[u8], 96 | ) -> (String, [u8; 32]) { 97 | // 16 here represents the lengths in bytes of the block height and time. 98 | let entropy_len = 16 + info.sender.to_string().len() + entropy.len(); 99 | let mut rng_entropy = Vec::with_capacity(entropy_len); 100 | rng_entropy.extend_from_slice(&env.block.height.to_be_bytes()); 101 | rng_entropy.extend_from_slice(&env.block.time.seconds().to_be_bytes()); 102 | rng_entropy.extend_from_slice(info.sender.as_bytes()); 103 | rng_entropy.extend_from_slice(entropy); 104 | 105 | let mut rng = ContractPrng::new(seed, &rng_entropy); 106 | 107 | let rand_slice = rng.rand_bytes(); 108 | 109 | let key = sha_256(&rand_slice); 110 | 111 | let viewing_key = VIEWING_KEY_PREFIX.to_string() + &general_purpose::STANDARD.encode(key); 112 | (viewing_key, rand_slice) 113 | } 114 | 115 | fn ct_slice_compare(s1: &[u8], s2: &[u8]) -> bool { 116 | bool::from(s1.ct_eq(s2)) 117 | } 118 | 119 | #[cfg(test)] 120 | mod tests { 121 | use super::*; 122 | 123 | use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info}; 124 | 125 | #[test] 126 | fn test_viewing_keys() { 127 | let account = "user-1".to_string(); 128 | 129 | let mut deps = mock_dependencies(); 130 | let env = mock_env(); 131 | let info = mock_info(account.as_str(), &[]); 132 | 133 | // VK not set yet: 134 | let result = ViewingKey::check(&deps.storage, &account, "fake key"); 135 | assert_eq!(result, Err(StdError::generic_err("unauthorized"))); 136 | 137 | ViewingKey::set_seed(&mut deps.storage, b"seed"); 138 | let viewing_key = ViewingKey::create(&mut deps.storage, &info, &env, &account, b"entropy"); 139 | 140 | let result = ViewingKey::check(&deps.storage, &account, &viewing_key); 141 | assert_eq!(result, Ok(())); 142 | 143 | // Create a key with the same entropy. Check that it's different 144 | let viewing_key_2 = 145 | ViewingKey::create(&mut deps.storage, &info, &env, &account, b"entropy"); 146 | assert_ne!(viewing_key, viewing_key_2); 147 | 148 | // VK set to another key: 149 | let result = ViewingKey::check(&deps.storage, &account, "fake key"); 150 | assert_eq!(result, Err(StdError::generic_err("unauthorized"))); 151 | 152 | let viewing_key = "custom key"; 153 | 154 | ViewingKey::set(&mut deps.storage, &account, viewing_key); 155 | 156 | let result = ViewingKey::check(&deps.storage, &account, viewing_key); 157 | assert_eq!(result, Ok(())); 158 | 159 | // VK set to another key: 160 | let result = ViewingKey::check(&deps.storage, &account, "fake key"); 161 | assert_eq!(result, Err(StdError::generic_err("unauthorized"))); 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![doc = include_str!("../Readme.md")] 2 | 3 | #[cfg(feature = "crypto")] 4 | pub use secret_toolkit_crypto as crypto; 5 | #[cfg(feature = "incubator")] 6 | pub use secret_toolkit_incubator as incubator; 7 | #[cfg(feature = "notification")] 8 | pub use secret_toolkit_notification as notification; 9 | #[cfg(feature = "permit")] 10 | pub use secret_toolkit_permit as permit; 11 | #[cfg(feature = "serialization")] 12 | pub use secret_toolkit_serialization as serialization; 13 | #[cfg(feature = "snip20")] 14 | pub use secret_toolkit_snip20 as snip20; 15 | #[cfg(feature = "snip721")] 16 | pub use secret_toolkit_snip721 as snip721; 17 | #[cfg(feature = "storage")] 18 | pub use secret_toolkit_storage as storage; 19 | #[cfg(feature = "utils")] 20 | pub use secret_toolkit_utils as utils; 21 | #[cfg(feature = "viewing-key")] 22 | pub use secret_toolkit_viewing_key as viewing_key; 23 | --------------------------------------------------------------------------------