templar_common/versioned_state/
macros.rs

1#[macro_export]
2macro_rules! impl_versioned_state {
3    ($contract: ident, $current_state: ty, $migrations: ty) => {
4        #[::near_sdk::near]
5        impl $crate::versioned_state::MigrateExternalInterface for $contract {
6            fn get_stored_state_version() -> u32 {
7                $crate::versioned_state::read_state_version()
8                    .unwrap_or_else(|e| ::near_sdk::env::panic_str(&e.to_string()))
9            }
10
11            fn get_target_state_version() -> u32 {
12                <$current_state as $crate::versioned_state::StateVersion>::VERSION
13            }
14
15            fn needs_migration() -> bool {
16                <$current_state as $crate::versioned_state::StateVersion>::needs_migration()
17                    .unwrap_or_else(|e| ::near_sdk::env::panic_str(&e.to_string()))
18            }
19        }
20
21        #[cfg_attr(target_arch = "wasm32", unsafe(no_mangle))]
22        #[cfg_attr(not(target_arch = "wasm32"), allow(dead_code))]
23        pub fn migrate() {
24            use near_sdk::env;
25            env::setup_panic_hook();
26
27            ::near_sdk::require!(
28                env::predecessor_account_id() == env::current_account_id(),
29                "migrate function is private",
30            );
31
32            // Stored-vs-target version — never `migrate_args` — decides whether to migrate, so a
33            // caller can't skip a required migration or slip a stale transform past a current contract.
34            let needs =
35                <$current_state as $crate::versioned_state::StateVersion>::needs_migration()
36                    .unwrap_or_else(|e| env::panic_str(&e.to_string()));
37
38            let input = env::input().unwrap_or_default();
39
40            if !needs {
41                ::near_sdk::require!(
42                    input.is_empty(),
43                    "no state migration is needed; migrate_args must be empty",
44                );
45                return;
46            }
47
48            // Accept either a single migration (legacy wire shape) or an ordered list, and run the
49            // whole chain in one receipt: each step's output version must feed the next step's
50            // input, the first must start at the stored version, and the last must land on target.
51            // The chain is fully validated before any transform runs, so an invalid chain reverts
52            // without writing state.
53            let migrations: Vec<$migrations> = $crate::versioned_state::parse_one_or_many(&input)
54                .unwrap_or_else(|e| env::panic_str(&e.to_string()));
55
56            $crate::versioned_state::run_migration_chain(
57                migrations,
58                <$current_state as $crate::versioned_state::StateVersion>::VERSION,
59            )
60            .unwrap_or_else(|e| env::panic_str(&e.to_string()));
61
62            // Defense in depth: the chain validated its endpoints against the args, but re-assert
63            // against live state so a run that somehow left state stranded reverts atomically.
64            ::near_sdk::require!(
65                !<$current_state as $crate::versioned_state::StateVersion>::needs_migration()
66                    .unwrap_or_else(|e| env::panic_str(&e.to_string())),
67                "state migration incomplete: stored version is still behind target",
68            );
69        }
70    };
71}
72
73pub use impl_versioned_state;