Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
A description of CosmWasm and the framework around it as implemented on Secret Network
Smart contracts on Secret Network follow the widely used CosmWasm framework. The framework is built as a module for the Cosmos SDK and allows developers to build smart contracts with Rust which compile to WebAssmebly (Wasm). This type of efficient and fast binary, combined with the power of a low-level typed coding language, makes CosmWasm a secure and efficient way for developers to write smart contracts. Contracts on Secret have a public binary just like any other smart contract network, only the contract input, output and state is encrypted. The contracts are executed in the Wasm VM as deployed via the Wasmd Cosmos SDK module on the Secret blockchain.
Secret Contracts slightly differ from any standard CosmWasm implementation as they handle encrypted data, secure enclaves, and the key design around that. Secret currently emulates CosmWasm v1.1.9 and is completely compatible over IBC with other V1.0+ Cosmos Smart contract networks.
Want to learn about how Secret handles the Contract-state encryption and other details that relate to how CosmWasm works on Secret? -- Read the implementation documentation here!
A Secret contract has 4 different components as listed below each serving a different function. A deeper overview of the implementation of these 4 components/files is given in the Contract-components section.
Instantiate - the logic that is ran once on initialization of the contract. This will usually be the initial conditions and configuration of the contract.
Execute - this is where the transactional logic resides. These are functions that modify contract data
Query - these are functions that are ran as read-only and cannot modify contract data
State/storage - the long-term storage of the contract. By default, data used by contracts will be lost between transactions.
Each contract will contain these basic building blocks, with their logic being built around how they work. You can read more about the interplay of Entry-points as depicted in the graphic below in this starter guide.
Want to get started with CosmWasm development? - Get started with our 4 part crash course. Dont have much time? - Try our guide on the millionaires problem instead! - It teaches everything about the CosmWasm framework, message types and environment setup in a single page.
An explainer of the Instantiate file inside of the CosmWasm code framework
The Instantiate function is the function that will run once (and only once) immediately upon initializing your smart contract after uploading it to the network. It is meant to set up the smart contract with all vital code that must be run first before any users have the ability to execute the contract. For those familiar with a solidity constructor on Ethereum, Instantiate serves the exact same purpose.
If you have any vital info to save to the state such as Config parameters or an admin/operator address to name a few options, those should probably be specified here. In addition, if your contract requires entropy for randomization or needs to register to receive a SNIP-20 token for payment, those operations should also be performed here, as other execution messages will rely on that data to be already instantiated.
Instantiate takes four arguments:
"deps"
: allows the contract to interact with the outside world by reading and updating the contract state, accessing other contract states, and using helper functions to work with contract addresses.
"env":
is an object that represents the current state of the blockchain when the contract is executed, including the blockchain's height, timestamp, and the address of the contract being called.
"info"
: metadata about the message that triggered the contract's execution, such as the sender's address and the native tokens that were sent with the message.
"msg":
is the message that triggers the contract's execution, which is currently represented by the "Empty" type in the codeblock above.
An explainer of the query file inside of the CosmWasm code framework
Query messages retrieve the relevant smart contract and execute the query message. The results of the query are then returned to the sender of the query message, providing users with information about the state of the blockchain and the smart contracts running on it.
Contracts can define query functions, or read-only operations meant for data-retrieval. Doing so allows contracts to expose rich, custom data endpoints with JSON responses instead of raw bytes from the low-level key-value store. Because the blockchain state cannot be changed, the node can directly run the query without a transaction.
Users can specify which query function alongside any arguments with a JSON QueryMsg
. Even though there is no gas fee, the query function’s execution is capped by gas determined by metered execution, which is not charged, as a form of spam protection.
An explainer on the varying storage frameworks for Secret contracts
CosmWasm storage uses a key-value storage design. Smart contracts can store data in binary, access it through a storage key, edit it, and save it. Similar to a HashMap, each storage key is associated with a specific piece of data stored in binary. The storage keys are formatted as references to byte arrays (&[u8]
).
One advantage of the key-value design is that a particular data value is only loaded when the user explicitly loads it using its storage key. This prevents any unnecessary data from being processed, saving resources.
Any type of data may be stored this way as long as the user can serialize/deserialize (serde) the data to/from binary. Doing this manually every single time is cumbersome and repetitive, this is why we have wrapper functions that does this serde process for us.
All the data is actually stored in deps.storage
, and the examples below show how to save/load data to/from there with a storage key.
Creating a storage key is simple, any way of generating a constant &[u8]
suffices. Developers often prefer generating these keys from strings as shown in the example below.
For example, the above key is likely used to store some data related to core configuration values of the contract. The convention is that storage keys are often all created in state.rs
, and then imported to contract.rs
. However, since storage keys are just constants, they could be declared anywhere in the contract.
The example above also highlights that storage keys are not meant to be secret nor hard to guess. Anyone who has the open source code can see what the storage keys are (and of course this is not enough for a user to load any data from the smart contract).
One common technique in smart contracts, especially when multiple types of data are being stored, is to create separate sub-stores with unique prefixes. Thus instead of directly dealing with storage, we wrap it and put all Foo
in a Storage with key "foo" + id
, and all Bar
in a Storage with key "bar" + id
. This lets us add multiple types of objects without too much cognitive overhead. Similar separation like Mongo collections or SQL tables.
Since we have different types for Storage
and ReadonlyStorage
, we use two different constructors:
Please note that only one mutable reference to the underlying store may be valid at one point. The compiler sees we do not ever use foos
after constructing bars
, so this example is valid. However, if we did use foos
again at the bottom, it would properly complain about violating unique mutable reference.
The takeaway is to create the PrefixedStorage
objects when needed and not to hang around to them too long.
As we divide our storage space into different subspaces or "buckets", we will quickly notice that each "bucket" works on a unique type. This leads to a lot of repeated serialization and deserialization boilerplate that can be removed. We do this by wrapping a Storage
with a type-aware TypedStorage
struct that provides us a higher-level access to the data.
Note that TypedStorage
itself does not implement the Storage
interface, so when combining with PrefixStorage
, make sure to wrap the prefix first.
Beyond the basic save
, load
, and may_load
, there is a higher-level API exposed, update
. Update
will load the data, apply an operation and save it again (if the operation was successful). It will also return any error that occurred, or the final state that was written if successful.
Since the above idiom (a subspace for a class of items) is so common and useful, and there is no easy way to return this from a function (bucket holds a reference to space, and cannot live longer than the local variable), the two are often combined into a Bucket
. A Bucket works just like the example above, except the creation can be in another function:
Singleton is another wrapper around the TypedStorage
API. There are cases when we don't need a whole subspace to hold arbitrary key-value lookup for typed data, but rather a single storage key. The simplest example is some configuration information for a contract. For example, in the name service example, there is a Bucket
to look up name to name data, but we also have a Singleton
to store global configuration - namely the price of buying a name.
Please note that in this context, the term "singleton" does not refer to the singleton pattern but a container for a single element.
Singleton
works just like Bucket
, except the save
, load
, update
methods don't take a key, and update
requires the object to already exist, so the closure takes type T
, rather than Option<T>
. (Use save
to create the object the first time). For Buckets
, we often don't know which keys exist, but Singleton
s should be initialized when the contract is instantiated.
Since the heart of much of the smart contract code is simply transformations upon some stored state, we may be able to just code the state transitions and let the TypedStorage
APIs take care of all the boilerplate.
CosmWasm storage is built on a key-value storage design, similar to a HashMap, where data is stored in binary format and accessed using storage keys represented as byte arrays (&[u8]
). Developers can serialize and deserialize data to/from binary, simplifying this process through wrapper functions. Storage keys are typically constants declared in state files and are not meant to be secret.
Prefixed storage allows for creating separate sub-stores with unique prefixes for organizing different data types, while Typed Storage provides a type-aware interface to reduce serialization boilerplate. CosmWasm also supports advanced abstractions like Buckets and Singletons. Buckets create subspaces for storing collections of items, while Singletons manage single elements, typically used for global contract configuration data. These APIs streamline storage handling, enabling efficient data management in smart contracts.
Now, we will explore some of these storage methods, such as PrefixedStorage
, Singleton
, and Keymap
, in more depth to see how they interact with on-chain data.
An explainer of the dependecies inside of the CosmWasm code framework
Deps
(or the mutable version DepsMut
) holds all external dependencies of the contract, and is designed to allow easy dependency injection at runtime. Those external dependencies are as follows:
As the name suggests, storage allows reading and writing data in a contract's memory. Secret Network contracts use a key-value store for reading and writing data and provide three easy-to-use methods to do so:
get(&self, key: &[u8]) -> Option<Vec<u8>>
reads data stored in the specified key
set(&self, key: &[u8], value: &[u8])
which writes data to the storage in the specified key
remove(&self, key: &[u8])
which deletes a key alongside its data.
You may have noticed that storage uses byte arrays exclusively to read and write data, so to make it more user-friendly there are certain libraries that the community has developed. You can learn more about this here.
Often the question of, "How much data can be stored in a contract?" is asked. The answer is that it’s complicated. Technically there’s no upper-bound on how much data a contract can store, but there are practical per-transaction limits.
Reading one byte of data “uses” 3 gas and writing one byte of data “uses” 30 gas, so with the current limit of 6_000_000 gas per block, the practical limit is that a contract can read — at most — 13.33Mb of data per transaction and can write — at most — 1.3Mb of data per transaction. This assumes that the contract doesn’t do anything but read and write data, the more complex the contract, the lower the amount of data you can read and write. Note that the limits are per transaction, you could write several gigabytes of data if you wanted to but it would take several transactions.
Api consists of a collection of callbacks to system functions defined outside of the wasm modules. Those are functions that are defined in the chain’s code, this allows calling useful functions without needing to include them in the contract’s code and assures a smaller binary size as well as a higher certainty that they’re going to be implemented correctly.
canonical_address(&self, human: &HumanAddr) -> StdResult<CanonicalAddr>
Takes a human readable address and returns a binary representation of it.
human_address(&self, canonical: &CanonicalAddr) -> StdResult<HumanAddr>
Takes a canonical address and returns a human readble address. This is the inverse of canonical_address
secp256k1_verify(&self, message_hash: &[u8], signature: &[u8], public_key: &[u8]) -> Result<bool, VerificationError>
Verifies message hashes (hashed unsing SHA-256) against a signature, with the public key of the signer, using the secp256k1 elliptic curve digital signature parametrization / algorithm.
secp256k1_recover_pubkey( &self, message_hash: &[u8], signature: &[u8], recovery_param: u8 ) -> Result<Vec, RecoverPubkeyError>
Recovers a public key from a message hash and a signature.
ed25519_verify( &self, message: &[u8], signature: &[u8], public_key: &[u8] ) -> Result<bool, VerificationError>
Verifies messages against a signature, with the public key of the signer, using the ed25519 elliptic curve digital signature parametrization / algorithm.
ed25519_batch_verify( &self, messages: &[&[u8]], signatures: &[&[u8]], public_keys: &[&[u8]] ) -> Result<bool, VerificationError>
Performs batch Ed25519 signature verification.
Batch verification asks whether all signatures in some set are valid, rather than asking whether each of them is valid. This allows sharing computations among all signature verifications, performing less work overall, at the cost of higher latency (the entire batch must complete), complexity of caller code (which must assemble a batch of signatures across work-items), and loss of the ability to easily pinpoint failing signatures.
This batch verification implementation is adaptive, in the sense that it detects multiple signatures created with the same verification key, and automatically coalesces terms in the final verification equation.
secp256k1_sign( &self, message: &[u8], private_key: &[u8] ) -> Result<Vec, SigningError>
Signs a message with a private key using the secp256k1 elliptic curve digital signature parametrization / algorithm.
ed25519_sign( &self, message: &[u8], private_key: &[u8] ) -> Result<Vec, SigningError>
Signs a message with a private key using the ed25519 elliptic curve digital signature parametrisation / algorithm.
(CosmWasm 1.0) debug
- a function that allows for debug prints, which can be used during development to print to STDOUT in a testnet or LocalSecret environment (replaces debug_print
from CosmWasm v0.10)
(New in Secret-Cosmwasm v1.1.10) gas_evaporate(evaporate: u32) -> u32
- this API function allows a contract to consume a set amount of gas. Use together with check_gas
in order to create contract calls that consume an exact amount of gas regardless of the code path taken. Documentation is here.
(New in Secret-Cosmwasm v1.1.10) check_gas() -> u64
- this API returns the current amount of gas consumed by a contract call.
Before we understand what the PrefixedStorage struct is, we should first understand the problem they attempt to solve. We may be tempted to think that the key-value model as described in Storage solves all our storage needs. And we'd technically be correct in thinking that. It is possible to efficiently store vast amounts of user data by just using the methods described in that page. However, we will see that doing this would be cumbersome for the developer in many cases. That's why there are many additional storage structures built on top of the methods described in Storage. Prefixed storage is an additional structure built on top of these methods for the convenience of the developer.
We will go over a storage problem and discuss how we might tackle it with the methods we learnt in Storage to get to the root of what prefixed storage actually is. Suppose that we have contract that stores a different password (String) for each wallet address that interacts with it (so that the wallets can later give that password when querying the contract). How would we want to store all these passwords so that they can be checked when it is time for queries?
Our first approach might be to use a hashmap that stores all the passwords, and save it as binary using the methods we already know. So we would add the following lines to the init
And then we can proceed to load, and add each users' password whenever a user sends the create_password
HandleMsg. Note that I'm assuming we would generate the &[u8]
key for the hashmap from the user's wallet address. This approach has a huge problem! That is whenever we want to add another wallet's password to the hashmap, we must load the entire hashmap with all the passwords stored inside it. This will increase gas costs as we gain thousands of users. Moreover, loading the hashmap for queries will strain the node.
We now realize that we can actually generate&[u8]
storage keys for each user wallet and save the password directly to deps.storage
as described in Storage. This way, we can use may_load
to check if a user has a password, and if he does, learn what it is without loading all the other passwords. Then we would use the following lines of code to save a password
This method does work! However, what if we want to save additional things that is associated to each user, such as token balances. Then using save on the same key would overwrite our previous data. The solution is to add a prefix to the storage key so that we know it belongs to the password property. One way to implement this is the following:
This works but can be cumbersome and ugly, especially if you need to build more functionalities around this idea. This is exactly what prefixed storage does in the background! Prefixed Storage is built to solve this problem while hiding away all the ugliness.
PrefixedStorage is a struct that keeps track of all the storage keys that are used to store various data under a shared namespace similar to described above. Prefixed storage is often used to keep track of storage keys of deps.storage
, but it can in fact be used to store the keys of other storage structures.
The compiler only allows one mutable reference to the underlying storage to be valid at one point. Thus, you cannot have more than one mutable Prefixed Storage objects at the same time.
Let's solve the password problem that was mentioned above with prefixed storage. The solution to this problem resembles viewing keys for permissioned viewing. You will learn more about this in the coming sections. We first define a namespace (prefix) in state.rs
.
The idea then is that all the wrapper functions that we've learnt in Storage also work on prefixed storage. We will only demonstrate save
and may_load
by example.
We would instantiate a mutable PrefixedStorage object using the following lines of code to save a new password
Notice that we are using the same storage wrapper functions from Storage on prefixed storage.
The reason why we can use the same wrapper functions is because Prefixed Storage implements the Storage trait, even though most of what it's doing is to add a prefix on top of the keys we provide to it.
We may use both load
and may_load
We may load data from a mutable Prefixed Storage instance with the following lines of code
In queries, we don't want to use PrefixedStorage struct. Instead we use ReadonlyPrefixedStorage struct. It works in the exact same way, except that you cannot save to it.
You can have as many ReadonlyPrefixedStorage objects as you want at the same time, unlike PrefixedStorage.
We can use the remove
wrapper function for this.
An explainer of the Execute file inside of the CosmWasm code framework
Execution Messages are contract messages that trigger the execution of a smart contract and perform specific operations, such as updating the contract state or transferring tokens. If you’re familiar with RPC or AJAX, you can think of execution messages as the code that runs when a remote procedure is called. On Secret Network, execution messages are usually designed to be functions that run as quickly as possible and exit as early as possible when any errors are encountered, as this will help save gas. You can learn more about contract optimization here.
The standard practice on Secret Network is to have an enum
with all the valid message types and reject all messages that don’t follow the usage pattern dictated in that enum; this enum is conventionally called ExecuteMsg
and is usually in a file called msg.rs
As mentioned earlier, the standard way to choose what the contract should execute is to look at which ExecuteMsg
is passed to the function. Let’s look at an example.
In this simple example, the code looks at the message passed, if the passage is the Increment
message, it calls the function try_increment
, otherwise, if the message is the Reset
message, it calls the function try_reset
with the count.
Remember, Rust has implicit returns for lines that don’t end in a semicolon, so the result of the two functions above is returned as the result of the execution message.
You may have noticed that the execution message takes two arguments in addition to the msg: deps
and env
. Let’s look at those more closely.
As the name perhaps implies, env
contains all the information about the environment the contract is running in, but what does that mean exactly? On Secret Network the properties available in the Env struct are as follows:
block: this contains all the information about the current block. This is the block height (height
), the current time as a unix timestamp (time
) and the chain id (chain_id
) such as secret-4 or pulsar-3.
message: contains information that was sent as part of the payload for the execution. This is the sender, or the wallet address of the person that called the handle function and sent_funds which contains a vector of native funds sent by the caller (SNIP-20s are not included).
contract: contains a property with the contract’s address.
contract_code_hash: this is a String containing the code hash of the current contract, it’s useful when registering with other contracts such as SNIP20s or SNIP721s or when working on a factory contract.
Now that you have an overview on what all those properties are, let’s take a look at a simple Execution Message.
The execution message above reads the state from the storage(Deps
) and increments the count property by one, then prints out the new count, if successful.
As mentioned previously, developers don’t really like working with raw binary data, so many resort to using more human friendly ways; you can find out more on the page about storage, here. But for the sake of this example, let’s look at what that config
function is doing.
The developers of this contract opted for using storage singletons. A storage singleton can be thought as a prefixed typed storage solution with simple-to-use methods. This allows the developer to define a structure for the data that needs to be stored and handles encoding end decoding it, so the developer doesn’t have to think about it. This is how it’s implemented in this contract:
The config
function returns a singleton over the config key using the State
struct as its type, this means that when reading and writing data from the storage, the singleton automatically serializes or deserializes the State struct.
You may have noticed that the return type for a handle message is Response
. Let's take a look at how Response
is defined:
In CosmWasm, the Ok
result and the Response
object are essential parts of handling contract execution and signaling the outcome to the blockchain. Here's a breakdown of how these work:
Ok
Result in CosmWasmIn Rust (and by extension, CosmWasm), functions that interact with the blockchain often return a Result
type. This is a way to express whether the function was successful or if it encountered an error.
Result<T, E>
: A Result
can either be:
Ok(T)
: Signifying the function executed successfully and returns a value of type T
.
Err(E)
: Signifying that an error of type E
occurred.
In the context of CosmWasm, the Ok
result generally signifies that a contract's execution completed successfully, returning a Response
object.
For example:
This line means that the contract execution was successful, and it returns a default Response
. In CosmWasm, we use StdResult<Response>
for contract execution functions.
A description of CosmWasm and the framework around it as implemented on Secret Network
This overview examines the core components of Secret Network smart contracts
Optimizing your code is especially important on blockchain due to the fact that inefficient code costs your users money and can jam up the entire network. With any function you make, try to complete it in as few steps as possible and avoid unnecessary computation. One of the most costly actions you can make is loading and saving data, thus you should ensure you are only saving what is needed. Aside from this, there are a few other tips we can provide.
When dealing with large sets of data, vectors and arrays can be especially problematic because in order to save load a single item within them, you must effectively load the entire vector and all of its contents, which naturally can amount to an enormous amount of inefficiency when you only need one item! There are several ways around this: Keymap, AppendStore, or even deconstructing vectors into a set of data keyed to a counter variable. Research each options pros and cons, and choose which one is best for your particular contract.
This hashmap-like storage structure uses generic typed keys to store objects.
A Keymap is hashmap-like storage structure that uses generic typed keys to store objects. It allows iteration with paging over keys and/or items without guaranteed ordering, although the order of insertion is preserved until you remove objects.
An example use-case for a keymap is if you want to contain a large amount of votes and iterate over them in the future. Since iterating over large amounts of data at once may be prohibitive, a keymap allows you to specify the amount of data that will be returned in each page. We will implement this voting example below to show how keymaps can be utilized in your own projects.
To import this package (and also the packages that we will be using for unit tests), add the following dependencies to your Cargo.toml
file
To import and initialize a keymap, use the following packages in your test environment:
Now let's write our first test!
Let's start by creating a function that inserts key-value pairs into a keymap. This code defines a test function called test_keymap_perf_vote_insert()
that creates a new Keymap
that maps Vec<u8>
keys to String
values, and then inserts 1000 key-value pairs into the Keymap
. Each key is a Vec<u8>
containing an integer from 0 to 999, and each value is a String
containing the text "I vote yes".
This test is passing! Which means that it asserts that the Keymap
contains 1000 key-value pairs, which map a number from 0 - 999 with the string "I vote yes."
Iterating with Keymaps
There are two methods that create an iterator in Keymap. These are .iter
and .iter_keys
. iter_keys
only iterates over the keys whereas iter
iterates over (key, item) pairs. Let's use iter
to test the iterator functionality of aKeymap
that maps Vec<u8>
keys to a struct Vote.
We are going to create a new Keymap
that maps Vec<u8>
keys to a struct Vote
that has two fields, vote
and person
. It then inserts two key-value pairs into the Keymap
, where the keys are the byte vectors b"key1".to_vec()
and b"key2".to_vec()
, and the values are Vote
structs containing information about the vote and the person who cast it.
We then use iter()
to check that the size of the iterator is 2, which means there are two key-value pairs in the Keymap:
Our test is passing! This means that the first element returned by the iterator matches the expected key-value pair for key1
, and that the second element returned by the iterator matches the expected key-value pair for key2
.
AppendStore is meant to replicate the functionality of an append list in a cosmwasm efficient manner. The length of the list is stored and used to pop/push items to the list. It also has a method to create a read only iterator.
This storage object also has the method remove
to remove a stored object from an arbitrary position in the list, but this can be extremely inefficient.
❗ Removing a storage object further from the tail gets increasingly inefficient. We recommend you use
pop
andpush
whenever possible.
The same conventions from Item
also apply here, that is:
AppendStore has to be told the type of the stored objects. And the serde optionally.
Every methods needs it's own reference to deps.storage
.
To import and initialize this storage object as a static constant in state.rs
, do the following:
❗ Initializing the object as const instead of static will also work but be less efficient since the variable won't be able to cache length data.
Often times we need these storage objects to be associated to a user address or some other key that is variable. In this case, you need not initialize a completely new AppendStore inside contract.rs
. Instead, you can create a new AppendStore by adding a suffix to an already existing AppendStore. This has the benefit of preventing you from having to rewrite the signature of the AppendStore. For example
Sometimes when iterating these objects, we may want to load the next n
objects at once. This may be prefered if the objects we are iterating over are cheap to store or if we know that multiple objects will need to be accessed back to back. In such cases we may want to change the internal indexing size (default of 1). We do this in state.rs
:
The main user facing methods to read/write to AppendStore are pop
, push
, get_len
, set_at
(which replaces data at a position within the length bound), clear
(which deletes all data in the storage), remove
(which removes an item in an arbitrary position, this is very inefficient). An extensive list of examples of these being used can be found inside the unit tests of AppendStore found in append_store.rs
.
AppendStore also implements a readonly iterator feature. This feature is also used to create a paging wrapper method called paging
. The way you create the iterator is:
More examples can be found in the unit tests. And the paging wrapper is used in the following manner:
An introduction to the differences between standard CosmWasm smart contracts and Secret Contracts
Secret Contracts are an implementation of the Rust-based library CosmWasm, while additionally enabling computation with private metadata. This brings unique use cases to Secret Network which aren’t possible on other blockchains, and also means that Secret Contracts have features that are different from standard CosmWasm contracts including: contract hashes, a specialized approach to iterators, raw queries, and contract migration.
An in-depth analysis of that can be deployed on a vanilla CosmWasm chain as well as Secret Network can be found in our
Contract hashes are required to bind a transaction to the specific contract being called. Otherwise, in a forked chain you could replace the called contract with one that decrypts and prints the input message.
Secret contracts do not have access to standard CosmWasm iterators because keys and values of data are encrypted when stored on-chain. This means that there is no logical structure that can allow iteration over these keys. However, Secret Labs has developed iterator functionality that can be imported using the
Raw queries are not available since raw data is encrypted when stored on-chain. Data can be decrypted only by the contract that owns the data, and so data is only available via permissioned contract queries (smart queries).
CW-plus, a toolkit for CosmWasm contracts is largely supported but might not be optimal to use in all cases. Secret has its own Toolkit called that has the majority of CW-plus functionality. Alternatively one can alter small parts of CW-plus around the missing Iterator feature to leverage that library as well.
For further examples demonstrating the usage of keymaps, refer to the .
Working with a Singleton in CosmWasm
In CosmWasm smart contracts, a Singleton
is a specialized structure that combines the concepts of PrefixedStorage and TypedStorage, allowing you to store and manage a single data entity efficiently under a specific storage key. This is particularly useful when you need to store just one instance of a state (such as contract configuration or global data) without the need for complex key management.
How Singleton Works:
storage: &'a mut dyn Storage
: The Singleton
holds a mutable reference to the storage layer, which is where the contract data is stored on-chain. This reference allows it to interact with and modify the stored data.
key: Vec<u8>
: The Singleton
operates on a single storage key, provided during its instantiation. This key is typically transformed using to_length_prefixed
to avoid collisions with other stored data, ensuring that the contract can safely manage this unique piece of state.
data: PhantomData<T>
: Rust’s PhantomData
is a marker for the data type T
without actually holding an instance of it. This allows the Singleton
to enforce that it works with a specific type (T
) that must implement Serialize
and DeserializeOwned
traits, which are essential for encoding/decoding the data stored in the singleton.
No Need for Multiple Keys: A Singleton
simplifies storage management by removing the need to manually manage multiple keys. The storage key is predefined in the constructor, and all operations (read, write, update) are tied to this single key. This ensures you are only ever working with one data object in storage.
Type-Safe Storage: The Singleton
uses TypedStorage functionality, which ensures that the data stored and retrieved from the storage is type-safe. This means you always work with the exact type you’ve defined (in this case, T
), reducing the risk of errors during serialization and deserialization.
Avoids Key Collisions: By applying the to_length_prefixed
transformation to the given storage key, the Singleton
ensures there are no name collisions in storage. This is particularly useful in larger contracts where multiple data points might otherwise share similar key names.
There are common wrapper functions that are included in the state.rs
of most secret contract templates, which are save,
load,
may_load,
remove,
and update
.
save
The save
function overwrites previously saved data associated to that storage key. save
will serialize the model and store, returns an error on serialization issues:
Purpose: It takes a reference to the storage and the data to be saved (usually the contract's state).
Usage: You call save
when you want to persist new data or update existing data in storage.
load
This function reads or retrieves data from the contract's state. It is commonly used when you need to access a piece of data stored in the contract, such as the current state or configuration.
Purpose: It accesses the storage and loads the state associated with the provided key.
Usage: load
is used whenever the contract needs to read existing data, such as fetching the configuration or state before making any modifications.
may_load
Purpose: This function attempts to retrieve and deserialize data from storage, returning Ok(None)
if no data exists at the given key. It returns an error only if deserialization fails.
Usage: may_load
is used when the existence of the data is optional. It allows you to safely check if data exists and handle cases where the data might not be present.
Behavior: It fetches the data associated with the key and attempts to deserialize it using may_deserialize
. If the key does not have any data, it returns Ok(None)
instead of an error, making it more forgiving than load
.
remove
Purpose: This function deletes or removes data associated with a specific key from the contract’s storage.
Usage: remove
is used when you no longer need to store certain data in the contract and want to free up space by removing the key-value pair from storage.
Behavior: It accesses the storage
object and deletes the data associated with the provided key.
update
This function loads, modifies, and then saves data back to the contract's state. It’s commonly used when the contract state needs to be modified, such as incrementing a counter or updating a setting.
Purpose: It allows for atomic updates to the state by accepting a closure (a function passed as an argument) that modifies the data and then saves it back to storage.
Usage: update
is ideal for changing existing data in the state, like incrementing a value, while ensuring that the storage operation is performed safely in one transaction.
save
: Persists or updates state in the contract’s storage.
load
: Retrieves and reads existing data from the contract’s storage.
may_load
: Fetches and deserializes data if it exists, returning Ok(None)
if no data is found at the key, and an error if deserialization fails.
remove
: Deletes the data associated with a specific key from storage.
update
: Modifies existing data in storage, allowing for atomic and safe updates.
These wrapper functions provide clean abstractions to manage the contract's state, making the contract logic easier to write and maintain. They are especially useful in ensuring that storage operations (which are critical for on-chain data) are handled efficiently and consistently across the contract.