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