Basic State Migration
The most straightforward migration approach uses a dedicated migrate method that transforms old state into new state format.
The Problem
Our base contract stores messages and payments separately:
Loading...
We want to update it to store payment information directly in each message:
Loading...
If we deploy this directly, the contract can't deserialize the old state because it expects PostedMessage with 4 fields but finds only 3.
The Solution: Migrate Method
Create a migration method that reads the old state and writes the new state:
Loading...
How It Works
1. Define Old State Structure
Loading...
Mirror the previous contract's state structure so we can deserialize it.
2. Read Old State
let mut old_state: OldState = env::state_read().expect("failed");
Deserialize the current state from storage using the old structure.
3. Transform Data
let mut new_messages: Vector<PostedMessage> = Vector::new(MESSAGES_PREFIX);
for (idx, posted) in old_state.messages.iter().enumerate() {
let payment = old_state.payments.get(idx as u64)
.expect("failed to get payment")
.clone();
new_messages.push(&PostedMessage {
payment,
premium: posted.premium,
sender: posted.sender.clone(),
text: posted.text.clone(),
})
}
Iterate through old messages, combining them with their corresponding payments.
4. Clean Up Old Data
old_state.payments.clear();
Remove the old payments vector from storage to avoid wasting storage space.
5. Return New State
Self { messages: new_messages }
Return the transformed state which gets written to storage.
Key Annotations
The migrate method uses special annotations:
#[private]
#[init(ignore_state)]
pub fn migrate() -> Self
#[private]: Only the contract account can call this method#[init(ignore_state)]: Allows rewriting state (normally initialization fails if state exists)
Deploying with Migration
Build the updated contract:
cd basic-updates/update
cargo near build
Deploy and migrate in one transaction:
cargo near deploy <contract-account> \
with-init-call migrate json-args {} \
prepaid-gas '100.0 Tgas' \
attached-deposit '0 NEAR' \
network-config testnet \
sign-with-keychain send
This:
- Deploys the new contract code
- Immediately calls
migrateto transform the state - Ensures the contract is usable after deployment
Verifying Migration
Check that messages now include payment information:
near contract call-function as-read-only \
<contract-account> get_messages \
json-args {} \
network-config testnet now
Expected result:
[
{
"payment": "90000000000000000000000",
"premium": false,
"sender": "user.testnet",
"text": "hello"
}
]
The old get_payments method no longer exists:
near contract call-function as-read-only \
<contract-account> get_payments \
json-args {} \
network-config testnet now
# Error: MethodNotFound
Important Considerations
Gas Requirements: Large state migrations may require more gas. Test with your data volume.
Atomicity: If migration fails, the entire transaction reverts. The old contract remains deployed.
Testing: Always test migrations on testnet with realistic data before mainnet deployment.
Backup: Consider exporting state before migration for additional safety.
Next, we'll explore versioned state that makes future migrations easier.