Skip to main content

iota_package_management/
lib.rs

1// Copyright (c) Mysten Labs, Inc.
2// Modifications Copyright (c) 2024 IOTA Stiftung
3// SPDX-License-Identifier: Apache-2.0
4
5use std::{
6    collections::HashMap,
7    fs::File,
8    path::{Path, PathBuf},
9    str::FromStr,
10};
11
12use anyhow::bail;
13use iota_sdk_types::{Address, ObjectId};
14use move_package::{
15    lock_file::{self, LockFile, schema::ManagedPackage},
16    resolution::resolution_graph::Package,
17    source_package::layout::SourcePackageLayout,
18};
19use move_symbol_pool::Symbol;
20
21pub mod system_package_versions;
22
23const PUBLISHED_AT_MANIFEST_FIELD: &str = "published-at";
24
25pub enum LockCommand {
26    Publish,
27    Upgrade,
28}
29
30#[derive(thiserror::Error, Debug, Clone)]
31pub enum PublishedAtError {
32    #[error("The 'published-at' field in Move.toml or Move.lock is invalid: {0:?}")]
33    Invalid(String),
34
35    #[error("The 'published-at' field is not present in Move.toml or Move.lock")]
36    NotPresent,
37
38    #[error(
39        "Conflicting 'published-at' addresses between Move.toml -- {id_manifest} -- and \
40         Move.lock -- {id_lock}"
41    )]
42    Conflict {
43        id_lock: ObjectId,
44        id_manifest: ObjectId,
45    },
46}
47
48/// Update the `Move.lock` file with automated address management info.
49///
50/// `chain_identifier` and `env_alias` identify the environment the published
51/// address is recorded under; callers resolve them from their wallet context.
52/// The `Move.lock` file principally records the published address (i.e.,
53/// package ID) of a package under that environment. See the `ManagedPackage`
54/// type in the lock file for a complete spec.
55pub fn update_lock_file_with_package_id(
56    chain_identifier: String,
57    env_alias: &str,
58    command: LockCommand,
59    install_dir: Option<PathBuf>,
60    lock_file: Option<PathBuf>,
61    original_id: ObjectId,
62    version: u64,
63) -> Result<(), anyhow::Error> {
64    let Some(lock_file) = lock_file else {
65        bail!(
66            "Expected a `Move.lock` file to exist after publishing \
67             package, but none found. Consider running `iota move build` to \
68             generate the `Move.lock` file in the package directory."
69        )
70    };
71    let install_dir = install_dir.unwrap_or(PathBuf::from("."));
72
73    let mut lock = LockFile::from(install_dir, &lock_file)?;
74    match command {
75        LockCommand::Publish => lock_file::schema::update_managed_address(
76            &mut lock,
77            env_alias,
78            lock_file::schema::ManagedAddressUpdate::Published {
79                chain_id: chain_identifier,
80                original_id: original_id.to_string(),
81            },
82        ),
83        LockCommand::Upgrade => lock_file::schema::update_managed_address(
84            &mut lock,
85            env_alias,
86            lock_file::schema::ManagedAddressUpdate::Upgraded {
87                latest_id: original_id.to_string(),
88                version,
89            },
90        ),
91    }?;
92    lock.commit(lock_file)?;
93    Ok(())
94}
95
96/// Sets the `original-published-id` in the Move.lock to the given `id`. This
97/// function provides a utility to manipulate the `original-published-id` during
98/// a package upgrade. For instance, we require graph resolution to resolve a
99/// `0x0` address for module names in the package to-be-upgraded, and the
100/// `Move.lock` value can be explicitly set to `0x0` in such cases (and reset
101/// otherwise). The function returns the existing `original-published-id`, if
102/// any.
103pub fn set_package_id(
104    package_path: &Path,
105    install_dir: Option<PathBuf>,
106    chain_id: &String,
107    id: Address,
108) -> Result<Option<Address>, anyhow::Error> {
109    let lock_file_path = package_path.join(SourcePackageLayout::Lock.path());
110    let Ok(mut lock_file) = File::open(lock_file_path.clone()) else {
111        return Ok(None);
112    };
113    let managed_package = ManagedPackage::read(&mut lock_file)
114        .ok()
115        .and_then(|m| m.into_iter().find(|(_, v)| v.chain_id == *chain_id));
116    let Some((env, v)) = managed_package else {
117        return Ok(None);
118    };
119    let install_dir = install_dir.unwrap_or(PathBuf::from("."));
120    let lock_for_update = LockFile::from(install_dir, &lock_file_path);
121    let Ok(mut lock_for_update) = lock_for_update else {
122        return Ok(None);
123    };
124    lock_file::schema::set_original_id(&mut lock_for_update, &env, &id.to_canonical_string(true))?;
125    lock_for_update.commit(lock_file_path)?;
126    let id = Address::from_str(&v.original_published_id)?;
127    Ok(Some(id))
128}
129
130/// Find the published on-chain ID in the `Move.lock` or `Move.toml` file.
131/// A chain ID of `None` means that we will only try to resolve a published ID
132/// from the Move.toml. The published ID is resolved from the `Move.toml` if the
133/// Move.lock does not exist. Else, we resolve from the `Move.lock`, where
134/// addresses are automatically managed. If conflicting IDs are found in the
135/// `Move.lock` vs. `Move.toml`, a "Conflict" error message returns.
136pub fn resolve_published_id(
137    package: &Package,
138    chain_id: Option<String>,
139) -> Result<ObjectId, PublishedAtError> {
140    // Look up a valid `published-at` in the `Move.toml` first, which we'll
141    // return if the Move.lock does not manage addresses.
142    let published_id_in_manifest = manifest_published_at(package);
143
144    match published_id_in_manifest {
145        Ok(_) | Err(PublishedAtError::NotPresent) => { /* nop */ }
146        Err(e) => {
147            return Err(e);
148        }
149    }
150
151    let lock = package.package_path.join(SourcePackageLayout::Lock.path());
152    let Ok(mut lock_file) = File::open(lock) else {
153        return published_id_in_manifest;
154    };
155
156    // Find the environment and ManagedPackage data for this chain_id.
157    let id_in_lock_for_chain_id =
158        lock_published_at(ManagedPackage::read(&mut lock_file).ok(), chain_id.as_ref());
159
160    match (id_in_lock_for_chain_id, published_id_in_manifest) {
161        (Ok(id_lock), Ok(id_manifest)) if id_lock != id_manifest => {
162            Err(PublishedAtError::Conflict {
163                id_lock,
164                id_manifest,
165            })
166        }
167
168        (Ok(id), _) | (_, Ok(id)) => Ok(id),
169
170        // We return early (above) if we failed to read the ID from the manifest for some reason
171        // other than it not being present, so at this point, we can defer to whatever error came
172        // from the lock file (Ok case is handled above).
173        (from_lock, Err(_)) => from_lock,
174    }
175}
176
177fn manifest_published_at(package: &Package) -> Result<ObjectId, PublishedAtError> {
178    let Some(value) = package
179        .source_package
180        .package
181        .custom_properties
182        .get(&Symbol::from(PUBLISHED_AT_MANIFEST_FIELD))
183    else {
184        return Err(PublishedAtError::NotPresent);
185    };
186
187    let id =
188        ObjectId::from_str(value.as_str()).map_err(|_| PublishedAtError::Invalid(value.clone()))?;
189
190    if id == ObjectId::ZERO {
191        Err(PublishedAtError::NotPresent)
192    } else {
193        Ok(id)
194    }
195}
196
197fn lock_published_at(
198    lock: Option<HashMap<String, ManagedPackage>>,
199    chain_id: Option<&String>,
200) -> Result<ObjectId, PublishedAtError> {
201    let (Some(lock), Some(chain_id)) = (lock, chain_id) else {
202        return Err(PublishedAtError::NotPresent);
203    };
204
205    let managed_package = lock
206        .into_values()
207        .find(|v| v.chain_id == *chain_id)
208        .ok_or(PublishedAtError::NotPresent)?;
209
210    let id = ObjectId::from_str(managed_package.latest_published_id.as_str())
211        .map_err(|_| PublishedAtError::Invalid(managed_package.latest_published_id.clone()))?;
212
213    if id == ObjectId::ZERO {
214        Err(PublishedAtError::NotPresent)
215    } else {
216        Ok(id)
217    }
218}