templar_common/
registry.rs

1use near_sdk::{
2    json_types::{Base58CryptoHash, U64},
3    near,
4};
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
7#[cfg_attr(feature = "rpc", derive(clap::ValueEnum))]
8#[near(serializers = [json, borsh])]
9pub enum DeployMode {
10    Normal,
11    GlobalHash,
12}
13
14impl std::fmt::Display for DeployMode {
15    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
16        match self {
17            DeployMode::Normal => write!(f, "Normal"),
18            DeployMode::GlobalHash => write!(f, "GlobalHash"),
19        }
20    }
21}
22
23#[derive(Clone, Debug, PartialEq, Eq)]
24#[near(serializers = [borsh, json])]
25pub struct Deployment {
26    pub version_key: String,
27    pub code_hash: Base58CryptoHash,
28    pub block_height: U64,
29}
30
31/// Where a registered version's code lives, and whether `deploy` can still use it.
32///
33/// `remove_version` soft-deletes by clearing the stored blob but keeping the key, and a
34/// `GlobalHash` version never stores one — so "has code" cannot tell the two apart.
35#[derive(Clone, Copy, Debug, PartialEq, Eq)]
36#[near(serializers = [json])]
37pub enum VersionAvailability {
38    /// Held in registry state. `code_len` sizes a chunked read of it.
39    Stored { code_len: u32 },
40    /// A NEAR global contract, resolvable by [`VersionInfo::code_hash`].
41    Global,
42    /// `remove_version` cleared the blob; the key remains but `deploy` panics.
43    Removed,
44}
45
46impl VersionAvailability {
47    pub fn is_deployable(self) -> bool {
48        matches!(self, Self::Stored { .. } | Self::Global)
49    }
50}
51
52/// A registered version.
53#[derive(Clone, Copy, Debug, PartialEq, Eq)]
54#[near(serializers = [json])]
55pub struct VersionInfo {
56    /// `sha256` of the code, computed by the registry when the version was added — unlike the
57    /// digest embedded in a version key, which is a convention the registry does not enforce.
58    pub code_hash: Base58CryptoHash,
59    pub availability: VersionAvailability,
60}
61
62/// A name's entry in the deployment map.
63///
64/// `deploy` refuses any name already present, so `Reserved` blocks a deployment just as
65/// `Deployed` does — a distinction [`Deployment`] alone cannot carry.
66#[derive(Clone, Debug, PartialEq, Eq)]
67#[near(serializers = [json])]
68pub enum RegistryEntryView {
69    /// Claimed by an in-flight deploy that has not finalized.
70    Reserved,
71    Deployed(Deployment),
72}
73
74impl RegistryEntryView {
75    pub fn deployment(&self) -> Option<&Deployment> {
76        match self {
77            Self::Reserved => None,
78            Self::Deployed(deployment) => Some(deployment),
79        }
80    }
81}
82
83#[cfg(test)]
84mod tests {
85    use near_sdk::serde_json::{self, json};
86    use rstest::rstest;
87
88    use super::*;
89
90    fn deployment() -> Deployment {
91        Deployment {
92            version_key: "market@1.5.0".to_string(),
93            code_hash: Base58CryptoHash::from([7u8; 32]),
94            block_height: 42.into(),
95        }
96    }
97
98    #[rstest]
99    #[case(VersionAvailability::Stored { code_len: 521_039 }, json!({ "Stored": { "code_len": 521_039 } }))]
100    #[case(VersionAvailability::Global, json!("Global"))]
101    #[case(VersionAvailability::Removed, json!("Removed"))]
102    fn availability_wire_format(
103        #[case] availability: VersionAvailability,
104        #[case] expected: serde_json::Value,
105    ) {
106        assert_eq!(serde_json::to_value(availability).unwrap(), expected);
107        assert_eq!(
108            serde_json::from_value::<VersionAvailability>(expected).unwrap(),
109            availability,
110        );
111    }
112
113    /// A `GlobalHash` version stores no code yet deploys fine, so "has code" would report it
114    /// alongside a soft-deleted one.
115    #[rstest]
116    #[case(VersionAvailability::Stored { code_len: 1 }, true)]
117    #[case(VersionAvailability::Global, true)]
118    #[case(VersionAvailability::Removed, false)]
119    fn deployability(#[case] availability: VersionAvailability, #[case] expected: bool) {
120        assert_eq!(availability.is_deployable(), expected);
121    }
122
123    #[test]
124    fn version_info_round_trips() {
125        let info = VersionInfo {
126            code_hash: Base58CryptoHash::from([3u8; 32]),
127            availability: VersionAvailability::Stored { code_len: 128 },
128        };
129        let value = serde_json::to_value(info).unwrap();
130        assert_eq!(serde_json::from_value::<VersionInfo>(value).unwrap(), info);
131    }
132
133    #[test]
134    fn reserved_is_distinguishable_from_deployed() {
135        let reserved = RegistryEntryView::Reserved;
136        assert_eq!(serde_json::to_value(&reserved).unwrap(), json!("Reserved"));
137        assert_eq!(reserved.deployment(), None);
138
139        let deployed = RegistryEntryView::Deployed(deployment());
140        assert_eq!(deployed.deployment(), Some(&deployment()));
141        assert_eq!(
142            serde_json::from_value::<RegistryEntryView>(serde_json::to_value(&deployed).unwrap())
143                .unwrap(),
144            deployed,
145        );
146    }
147}