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::{Context, bail};
13use iota_json_rpc_types::{IotaTransactionBlockResponse, get_new_package_obj_from_response};
14use iota_sdk::wallet_context::WalletContext;
15use iota_types::base_types::{IotaAddress, ObjectID};
16use move_package::{
17    lock_file::{self, LockFile, schema::ManagedPackage},
18    resolution::resolution_graph::Package,
19    source_package::layout::SourcePackageLayout,
20};
21use move_symbol_pool::Symbol;
22
23pub mod system_package_versions;
24
25const PUBLISHED_AT_MANIFEST_FIELD: &str = "published-at";
26
27pub enum LockCommand {
28    Publish,
29    Upgrade,
30}
31
32#[derive(thiserror::Error, Debug, Clone)]
33pub enum PublishedAtError {
34    #[error("The 'published-at' field in Move.toml or Move.lock is invalid: {0:?}")]
35    Invalid(String),
36
37    #[error("The 'published-at' field is not present in Move.toml or Move.lock")]
38    NotPresent,
39
40    #[error(
41        "Conflicting 'published-at' addresses between Move.toml -- {id_manifest} -- and \
42         Move.lock -- {id_lock}"
43    )]
44    Conflict {
45        id_lock: ObjectID,
46        id_manifest: ObjectID,
47    },
48}
49
50/// Update the `Move.lock` file with automated address management info.
51/// Expects a wallet context, the publish or upgrade command, its response.
52/// The `Move.lock` principally file records the published address (i.e.,
53/// package ID) of a package under an environment determined by the wallet
54/// context config. See the `ManagedPackage` type in the lock file for a
55/// complete spec.
56pub async fn update_lock_file(
57    context: &WalletContext,
58    command: LockCommand,
59    install_dir: Option<PathBuf>,
60    lock_file: Option<PathBuf>,
61    response: &IotaTransactionBlockResponse,
62) -> Result<(), anyhow::Error> {
63    let object_ref = get_new_package_obj_from_response(response).context(
64        "Expected a valid published package response but didn't see \
65         one when attempting to update the `Move.lock`.",
66    )?;
67    update_lock_file_with_package_id(
68        context,
69        command,
70        install_dir,
71        lock_file,
72        object_ref.object_id,
73        object_ref.version.as_u64(),
74    )
75    .await
76}
77
78/// Update the `Move.lock` file with automated address management info.
79/// This variant accepts the package ID and version directly, allowing for
80/// updates when a single transaction publishes multiple packages.
81/// Expects a wallet context, the publish or upgrade command, and the package
82/// details. The `Move.lock` principally file records the published address
83/// (i.e., package ID) of a package under an environment determined by the
84/// wallet context config. See the `ManagedPackage` type in the lock file for a
85/// complete spec.
86pub async fn update_lock_file_with_package_id(
87    context: &WalletContext,
88    command: LockCommand,
89    install_dir: Option<PathBuf>,
90    lock_file: Option<PathBuf>,
91    original_id: ObjectID,
92    version: u64,
93) -> Result<(), anyhow::Error> {
94    let chain_identifier = context
95        .get_client()
96        .await
97        .context("Network issue: couldn't use client to connect to chain when updating Move.lock")?
98        .read_api()
99        .get_chain_identifier()
100        .await
101        .context("Network issue: couldn't determine chain identifier for updating Move.lock")?;
102
103    let Some(lock_file) = lock_file else {
104        bail!(
105            "Expected a `Move.lock` file to exist after publishing \
106             package, but none found. Consider running `iota move build` to \
107             generate the `Move.lock` file in the package directory."
108        )
109    };
110    let install_dir = install_dir.unwrap_or(PathBuf::from("."));
111    let env = context.active_env().context(
112        "Could not resolve environment from active wallet context. \
113         Try ensure `iota client active-env` is valid.",
114    )?;
115
116    let mut lock = LockFile::from(install_dir, &lock_file)?;
117    match command {
118        LockCommand::Publish => lock_file::schema::update_managed_address(
119            &mut lock,
120            env.alias(),
121            lock_file::schema::ManagedAddressUpdate::Published {
122                chain_id: chain_identifier,
123                original_id: original_id.to_string(),
124            },
125        ),
126        LockCommand::Upgrade => lock_file::schema::update_managed_address(
127            &mut lock,
128            env.alias(),
129            lock_file::schema::ManagedAddressUpdate::Upgraded {
130                latest_id: original_id.to_string(),
131                version,
132            },
133        ),
134    }?;
135    lock.commit(lock_file)?;
136    Ok(())
137}
138
139/// Sets the `original-published-id` in the Move.lock to the given `id`. This
140/// function provides a utility to manipulate the `original-published-id` during
141/// a package upgrade. For instance, we require graph resolution to resolve a
142/// `0x0` address for module names in the package to-be-upgraded, and the
143/// `Move.lock` value can be explicitly set to `0x0` in such cases (and reset
144/// otherwise). The function returns the existing `original-published-id`, if
145/// any.
146pub fn set_package_id(
147    package_path: &Path,
148    install_dir: Option<PathBuf>,
149    chain_id: &String,
150    id: IotaAddress,
151) -> Result<Option<IotaAddress>, anyhow::Error> {
152    let lock_file_path = package_path.join(SourcePackageLayout::Lock.path());
153    let Ok(mut lock_file) = File::open(lock_file_path.clone()) else {
154        return Ok(None);
155    };
156    let managed_package = ManagedPackage::read(&mut lock_file)
157        .ok()
158        .and_then(|m| m.into_iter().find(|(_, v)| v.chain_id == *chain_id));
159    let Some((env, v)) = managed_package else {
160        return Ok(None);
161    };
162    let install_dir = install_dir.unwrap_or(PathBuf::from("."));
163    let lock_for_update = LockFile::from(install_dir, &lock_file_path);
164    let Ok(mut lock_for_update) = lock_for_update else {
165        return Ok(None);
166    };
167    lock_file::schema::set_original_id(&mut lock_for_update, &env, &id.to_canonical_string(true))?;
168    lock_for_update.commit(lock_file_path)?;
169    let id = IotaAddress::from_str(&v.original_published_id)?;
170    Ok(Some(id))
171}
172
173/// Find the published on-chain ID in the `Move.lock` or `Move.toml` file.
174/// A chain ID of `None` means that we will only try to resolve a published ID
175/// from the Move.toml. The published ID is resolved from the `Move.toml` if the
176/// Move.lock does not exist. Else, we resolve from the `Move.lock`, where
177/// addresses are automatically managed. If conflicting IDs are found in the
178/// `Move.lock` vs. `Move.toml`, a "Conflict" error message returns.
179pub fn resolve_published_id(
180    package: &Package,
181    chain_id: Option<String>,
182) -> Result<ObjectID, PublishedAtError> {
183    // Look up a valid `published-at` in the `Move.toml` first, which we'll
184    // return if the Move.lock does not manage addresses.
185    let published_id_in_manifest = manifest_published_at(package);
186
187    match published_id_in_manifest {
188        Ok(_) | Err(PublishedAtError::NotPresent) => { /* nop */ }
189        Err(e) => {
190            return Err(e);
191        }
192    }
193
194    let lock = package.package_path.join(SourcePackageLayout::Lock.path());
195    let Ok(mut lock_file) = File::open(lock) else {
196        return published_id_in_manifest;
197    };
198
199    // Find the environment and ManagedPackage data for this chain_id.
200    let id_in_lock_for_chain_id =
201        lock_published_at(ManagedPackage::read(&mut lock_file).ok(), chain_id.as_ref());
202
203    match (id_in_lock_for_chain_id, published_id_in_manifest) {
204        (Ok(id_lock), Ok(id_manifest)) if id_lock != id_manifest => {
205            Err(PublishedAtError::Conflict {
206                id_lock,
207                id_manifest,
208            })
209        }
210
211        (Ok(id), _) | (_, Ok(id)) => Ok(id),
212
213        // We return early (above) if we failed to read the ID from the manifest for some reason
214        // other than it not being present, so at this point, we can defer to whatever error came
215        // from the lock file (Ok case is handled above).
216        (from_lock, Err(_)) => from_lock,
217    }
218}
219
220fn manifest_published_at(package: &Package) -> Result<ObjectID, PublishedAtError> {
221    let Some(value) = package
222        .source_package
223        .package
224        .custom_properties
225        .get(&Symbol::from(PUBLISHED_AT_MANIFEST_FIELD))
226    else {
227        return Err(PublishedAtError::NotPresent);
228    };
229
230    let id =
231        ObjectID::from_str(value.as_str()).map_err(|_| PublishedAtError::Invalid(value.clone()))?;
232
233    if id == ObjectID::ZERO {
234        Err(PublishedAtError::NotPresent)
235    } else {
236        Ok(id)
237    }
238}
239
240fn lock_published_at(
241    lock: Option<HashMap<String, ManagedPackage>>,
242    chain_id: Option<&String>,
243) -> Result<ObjectID, PublishedAtError> {
244    let (Some(lock), Some(chain_id)) = (lock, chain_id) else {
245        return Err(PublishedAtError::NotPresent);
246    };
247
248    let managed_package = lock
249        .into_values()
250        .find(|v| v.chain_id == *chain_id)
251        .ok_or(PublishedAtError::NotPresent)?;
252
253    let id = ObjectID::from_str(managed_package.latest_published_id.as_str())
254        .map_err(|_| PublishedAtError::Invalid(managed_package.latest_published_id.clone()))?;
255
256    if id == ObjectID::ZERO {
257        Err(PublishedAtError::NotPresent)
258    } else {
259        Ok(id)
260    }
261}