templar_common/
registry.rs

1use near_sdk::{
2    json_types::{Base58CryptoHash, Base64VecU8, U64},
3    near,
4};
5
6/// Store the wasm or publish it as a global contract — the only open question once the bytes are
7/// in hand, which is why `registry.addArtifactVersion` still takes this.
8///
9/// [`VersionSource`] supersedes it on the `add_version` wire, where a third answer (a hash for code
10/// already on chain) is possible and the bytes are not a given.
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
12#[near(serializers = [json, borsh])]
13pub enum DeployMode {
14    Normal,
15    GlobalHash,
16}
17
18/// Where a version's code comes from, as one value.
19///
20/// Supersedes the `(DeployMode, Vec<u8>)` pair, which admitted a combination that means nothing:
21/// `Normal` alongside bytes already published as a global contract. Modelled on
22/// [`crate::upgrade::UpgradeSource`], which draws the same distinction for the upgrade path.
23///
24/// Borsh tags are pinned via explicit discriminants (`use_discriminant`): `tmplrmgr` plan files
25/// persist these args as opaque borsh, so the tag must not track declaration order. Discriminants
26/// 0 and 1 match `DeployMode::Normal`/`GlobalHash`, and [`Base64VecU8`] is a transparent borsh
27/// newtype over `Vec<u8>` — so `(version_key, Stored(code))` is byte-identical to the older
28/// `(version_key, DeployMode::Normal, code)`. `borsh_is_wire_compatible_with_deploy_mode` pins this.
29#[derive(Debug, Clone, PartialEq, Eq)]
30#[near(serializers = [json, borsh(use_discriminant = true)])]
31#[repr(u8)]
32pub enum VersionSource {
33    /// WASM held in registry state; `deploy` copies it onto each account.
34    Stored(Base64VecU8) = 0,
35    /// WASM published as a new global contract; the registry keeps only the hash.
36    PublishGlobal(Base64VecU8) = 1,
37    /// A global contract already on chain, by code hash. No publish cost.
38    ExistingGlobal(Base58CryptoHash) = 2,
39}
40
41impl std::fmt::Display for DeployMode {
42    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
43        match self {
44            DeployMode::Normal => write!(f, "Normal"),
45            DeployMode::GlobalHash => write!(f, "GlobalHash"),
46        }
47    }
48}
49
50#[derive(Clone, Debug, PartialEq, Eq)]
51#[near(serializers = [borsh, json])]
52pub struct Deployment {
53    pub version_key: String,
54    pub code_hash: Base58CryptoHash,
55    pub block_height: U64,
56}
57
58/// Where a registered version's code lives, and whether `deploy` can still use it.
59///
60/// `remove_version` soft-deletes by clearing the stored blob but keeping the key, and a
61/// `GlobalHash` version never stores one — so "has code" cannot tell the two apart.
62#[derive(Clone, Copy, Debug, PartialEq, Eq)]
63#[near(serializers = [json])]
64pub enum VersionAvailability {
65    /// Held in registry state. `code_len` sizes a chunked read of it.
66    Stored { code_len: u32 },
67    /// A NEAR global contract, resolvable by [`VersionInfo::code_hash`].
68    Global,
69    /// `remove_version` cleared the blob; the key remains but `deploy` panics.
70    Removed,
71}
72
73impl VersionAvailability {
74    pub fn is_deployable(self) -> bool {
75        matches!(self, Self::Stored { .. } | Self::Global)
76    }
77}
78
79/// A registered version.
80#[derive(Clone, Copy, Debug, PartialEq, Eq)]
81#[near(serializers = [json])]
82pub struct VersionInfo {
83    /// `sha256` of the code, computed by the registry when the version was added — unlike the
84    /// digest embedded in a version key, which is a convention the registry does not enforce.
85    pub code_hash: Base58CryptoHash,
86    pub availability: VersionAvailability,
87}
88
89/// A name's entry in the deployment map.
90///
91/// `deploy` refuses any name already present, so `Reserved` blocks a deployment just as
92/// `Deployed` does — a distinction [`Deployment`] alone cannot carry.
93#[derive(Clone, Debug, PartialEq, Eq)]
94#[near(serializers = [json])]
95pub enum RegistryEntryView {
96    /// Claimed by an in-flight deploy that has not finalized.
97    Reserved,
98    Deployed(Deployment),
99}
100
101impl RegistryEntryView {
102    pub fn deployment(&self) -> Option<&Deployment> {
103        match self {
104            Self::Reserved => None,
105            Self::Deployed(deployment) => Some(deployment),
106        }
107    }
108}
109
110#[cfg(test)]
111mod tests {
112    use near_sdk::serde_json::{self, json};
113    use rstest::rstest;
114
115    use super::*;
116
117    fn deployment() -> Deployment {
118        Deployment {
119            version_key: "market@1.5.0".to_string(),
120            code_hash: Base58CryptoHash::from([7u8; 32]),
121            block_height: 42.into(),
122        }
123    }
124
125    #[rstest]
126    #[case(VersionAvailability::Stored { code_len: 521_039 }, json!({ "Stored": { "code_len": 521_039 } }))]
127    #[case(VersionAvailability::Global, json!("Global"))]
128    #[case(VersionAvailability::Removed, json!("Removed"))]
129    fn availability_wire_format(
130        #[case] availability: VersionAvailability,
131        #[case] expected: serde_json::Value,
132    ) {
133        assert_eq!(serde_json::to_value(availability).unwrap(), expected);
134        assert_eq!(
135            serde_json::from_value::<VersionAvailability>(expected).unwrap(),
136            availability,
137        );
138    }
139
140    /// A `GlobalHash` version stores no code yet deploys fine, so "has code" would report it
141    /// alongside a soft-deleted one.
142    #[rstest]
143    #[case(VersionAvailability::Stored { code_len: 1 }, true)]
144    #[case(VersionAvailability::Global, true)]
145    #[case(VersionAvailability::Removed, false)]
146    fn deployability(#[case] availability: VersionAvailability, #[case] expected: bool) {
147        assert_eq!(availability.is_deployable(), expected);
148    }
149
150    #[test]
151    fn version_info_round_trips() {
152        let info = VersionInfo {
153            code_hash: Base58CryptoHash::from([3u8; 32]),
154            availability: VersionAvailability::Stored { code_len: 128 },
155        };
156        let value = serde_json::to_value(info).unwrap();
157        assert_eq!(serde_json::from_value::<VersionInfo>(value).unwrap(), info);
158    }
159
160    /// The fold from `(DeployMode, Vec<u8>)` to one [`VersionSource`] must not move a byte, or
161    /// every 1.1.0+ registry already on chain stops accepting `add_version`.
162    #[rstest]
163    #[case(DeployMode::Normal, VersionSource::Stored(Base64VecU8(vec![0xde, 0xad])))]
164    #[case(DeployMode::GlobalHash, VersionSource::PublishGlobal(Base64VecU8(vec![0xde, 0xad])))]
165    fn borsh_is_wire_compatible_with_deploy_mode(
166        #[case] mode: DeployMode,
167        #[case] source: VersionSource,
168    ) {
169        let version_key = "market@1.5.0";
170        assert_eq!(
171            near_sdk::borsh::to_vec(&(version_key, &source)).unwrap(),
172            near_sdk::borsh::to_vec(&(version_key, mode, vec![0xdeu8, 0xad])).unwrap(),
173        );
174    }
175
176    /// Golden bytes for the persisted tags. Plan files hold these opaquely, so a discriminant that
177    /// shifted with declaration order would silently repoint an already-written plan.
178    #[rstest]
179    #[case(VersionSource::Stored(Base64VecU8(vec![0xaa])), vec![0, 1, 0, 0, 0, 0xaa])]
180    #[case(VersionSource::PublishGlobal(Base64VecU8(vec![0xaa])), vec![1, 1, 0, 0, 0, 0xaa])]
181    #[case(
182        VersionSource::ExistingGlobal(Base58CryptoHash::from([7u8; 32])),
183        [&[2u8][..], &[7u8; 32][..]].concat(),
184    )]
185    fn borsh_discriminants_are_stable(#[case] source: VersionSource, #[case] expected: Vec<u8>) {
186        assert_eq!(near_sdk::borsh::to_vec(&source).unwrap(), expected);
187    }
188
189    #[rstest]
190    #[case(VersionSource::Stored(Base64VecU8(vec![1, 2, 3])))]
191    #[case(VersionSource::PublishGlobal(Base64VecU8(vec![1, 2, 3])))]
192    #[case(VersionSource::ExistingGlobal(Base58CryptoHash::from([9u8; 32])))]
193    fn version_source_round_trips(#[case] source: VersionSource) {
194        let bytes = near_sdk::borsh::to_vec(&source).unwrap();
195        assert_eq!(
196            near_sdk::borsh::from_slice::<VersionSource>(&bytes).unwrap(),
197            source,
198        );
199        let value = serde_json::to_value(&source).unwrap();
200        assert_eq!(
201            serde_json::from_value::<VersionSource>(value).unwrap(),
202            source,
203        );
204    }
205
206    #[test]
207    fn reserved_is_distinguishable_from_deployed() {
208        let reserved = RegistryEntryView::Reserved;
209        assert_eq!(serde_json::to_value(&reserved).unwrap(), json!("Reserved"));
210        assert_eq!(reserved.deployment(), None);
211
212        let deployed = RegistryEntryView::Deployed(deployment());
213        assert_eq!(deployed.deployment(), Some(&deployment()));
214        assert_eq!(
215            serde_json::from_value::<RegistryEntryView>(serde_json::to_value(&deployed).unwrap())
216                .unwrap(),
217            deployed,
218        );
219    }
220}