State Versioning with Enums
State versioning uses Rust enums to allow multiple versions of your data structures to coexist, making future updates much simpler.
The Versioning Approach
Instead of storing messages directly, wrap them in a versioned enum:
Loading...
This lets you add new versions without breaking existing data.
Base Contract with Versioning
Store versioned messages:
Loading...
When adding messages, wrap them in the version enum:
Loading...
Retrieving Messages
Convert versioned messages to a specific version when reading:
Loading...
The From implementation handles the conversion:
Loading...
Adding a New Version
When you need to update the message structure, add a new enum variant:
Loading...
Update the enum to include both versions:
Loading...
Automatic Migration
The key benefit: implement From to automatically convert old versions:
Loading...
When reading messages, V1 messages automatically convert to V2 with default values for new fields.
Updated Contract
The updated contract uses V2 for new messages:
Loading...
Reading still works seamlessly:
Loading...
No Migration Method Needed
Deploy the updated contract directly:
cd enum-updates/update
cargo near build
cargo near deploy <contract-account> \
without-init-call \
network-config testnet \
sign-with-keychain send
No migration method needed! Old V1 messages coexist with new V2 messages.
Testing the Update
Add a new message:
near contract call-function as-transaction \
<contract-account> add_message \
json-args '{"text": "new message"}' \
prepaid-gas '100.0 Tgas' \
attached-deposit '0.1 NEAR' \
sign-as <your-account> \
network-config testnet \
sign-with-keychain send
Retrieve all messages:
near contract call-function as-read-only \
<contract-account> get_messages \
json-args {} \
network-config testnet now
Result shows both old and new messages in V2 format:
[
{
"payment": "0",
"premium": false,
"sender": "user1.testnet",
"text": "old message"
},
{
"payment": "100000000000000000000000",
"premium": true,
"sender": "user2.testnet",
"text": "new message"
}
]
Old V1 messages get payment: "0" automatically.
Advantages of Versioning
No downtime: Deploy updates without migration methods
Gradual migration: Old data converts on-read, not all at once
Gas efficiency: No large migration transaction needed
Flexibility: Easy to add multiple versions over time
Safety: Old data remains unchanged in storage
When to Use Versioning
Versioning works best when:
- You anticipate future schema changes
- You want zero-downtime updates
- Reading old data with default values is acceptable
- Storage efficiency isn't critical (versions add overhead)
Versioning Multiple Structures
You can version multiple structures:
pub enum VersionedUser {
V1(UserV1),
V2(UserV2),
}
pub enum VersionedPost {
V1(PostV1),
V2(PostV2),
}
pub struct Contract {
users: Vector<VersionedUser>,
posts: Vector<VersionedPost>,
}
Each structure versions independently.
Handling Many Versions
As versions accumulate, consider consolidating:
// After several versions, manually migrate V1-V3 to V4
pub fn consolidate_old_versions(&mut self) {
let mut new_messages = Vector::new(b"m");
for msg in self.messages.iter() {
let v4_msg = match msg {
VersionedMessage::V1(m) => convert_v1_to_v4(m),
VersionedMessage::V2(m) => convert_v2_to_v4(m),
VersionedMessage::V3(m) => convert_v3_to_v4(m),
VersionedMessage::V4(m) => m,
};
new_messages.push(v4_msg);
}
self.messages = new_messages;
}
Next, we'll explore self-updating contracts that can deploy and migrate themselves.