1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
//! Migrate pattern implements methods to change the storage representation of a struct.
//!
//! The migration controller takes the old and new schema and deserializes
//! the contract state from the old schema. The [`on_migrate`][`MigrateHook::on_migrate`]
//! method takes this state and replaces it with the new schema.
//! [`MigrateExternal`] exposes this functionality publicly.
//!
//! The crate exports a [derive macro](near_contract_tools_macros::Migrate)
//! that derives a default implementation for migration.
//!
//! Note: [`MigrateHook`] must be implemented by the user and is not derived
//! by default. It must convert data in the old schema to the new schema without
//! failing. For a complete example checkout [upgrade_new.rs](https://github.com/NEARFoundation/near-contract-tools/blob/develop/workspaces-tests/src/bin/upgrade_new.rs)
//! in workspace-tests.
//!
//! # Safety
//! The contract state must conform to the old schema otherwise deserializing it
//! will fail and throw an error.
#![allow(missing_docs)] // #[ext_contract(...)] does not play nicely with clippy

use near_sdk::{
    borsh::{BorshDeserialize, BorshSerialize},
    env, ext_contract,
};

// TODO: Migration events?
// *Possibly* unnecessary, since the salient occurence will probably be the instigating event (e.g. a code upgrade)
// Alternative solution: post-migration hook/callback so that the author can implement their own events if desired

/// Conversion between two storage schemas
pub trait MigrateController {
    /// Schema that currently exists in storage, to convert from
    type OldSchema: BorshDeserialize;
    /// Schema that will be used henceforth, to convert into
    type NewSchema: BorshSerialize;

    /// Deserializes the old schema from storage.
    ///
    /// It is probably not necessary to override this function.
    fn deserialize_old_schema() -> Self::OldSchema {
        env::state_read::<Self::OldSchema>()
            .unwrap_or_else(|| env::panic_str("Failed to deserialize old state"))
    }
}

/// Called on migration. Must be implemented by the user. (The derive macro
/// does not implement this for you.)
pub trait MigrateHook: MigrateController {
    /// Receives the old schema deserialized from storage as well as optional
    /// arguments from caller, and replaces it with the new schema.
    fn on_migrate(
        old_schema: <Self as MigrateController>::OldSchema,
    ) -> <Self as MigrateController>::NewSchema;
}

/// Migrate-able contracts expose this trait publicly
#[ext_contract(ext_migrate)]
pub trait MigrateExternal {
    /// Perform the migration with optional arguments
    fn migrate() -> Self;
}