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 chain_identifier = context
65        .get_client()
66        .await
67        .context("Network issue: couldn't use client to connect to chain when updating Move.lock")?
68        .read_api()
69        .get_chain_identifier()
70        .await
71        .context("Network issue: couldn't determine chain identifier for updating Move.lock")?;
72
73    let (original_id, version, _) = get_new_package_obj_from_response(response).context(
74        "Expected a valid published package response but didn't see \
75         one when attempting to update the `Move.lock`.",
76    )?;
77    let Some(lock_file) = lock_file else {
78        bail!(
79            "Expected a `Move.lock` file to exist after publishing \
80             package, but none found. Consider running `iota move build` to \
81             generate the `Move.lock` file in the package directory."
82        )
83    };
84    let install_dir = install_dir.unwrap_or(PathBuf::from("."));
85    let env = context.active_env().context(
86        "Could not resolve environment from active wallet context. \
87         Try ensure `iota client active-env` is valid.",
88    )?;
89
90    let mut lock = LockFile::from(install_dir.clone(), &lock_file)?;
91    match command {
92        LockCommand::Publish => lock_file::schema::update_managed_address(
93            &mut lock,
94            env.alias(),
95            lock_file::schema::ManagedAddressUpdate::Published {
96                chain_id: chain_identifier,
97                original_id: original_id.to_string(),
98            },
99        ),
100        LockCommand::Upgrade => lock_file::schema::update_managed_address(
101            &mut lock,
102            env.alias(),
103            lock_file::schema::ManagedAddressUpdate::Upgraded {
104                latest_id: original_id.to_string(),
105                version: version.into(),
106            },
107        ),
108    }?;
109    lock.commit(lock_file)?;
110    Ok(())
111}
112
113/// Sets the `original-published-id` in the Move.lock to the given `id`. This
114/// function provides a utility to manipulate the `original-published-id` during
115/// a package upgrade. For instance, we require graph resolution to resolve a
116/// `0x0` address for module names in the package to-be-upgraded, and the
117/// `Move.lock` value can be explicitly set to `0x0` in such cases (and reset
118/// otherwise). The function returns the existing `original-published-id`, if
119/// any.
120pub fn set_package_id(
121    package_path: &Path,
122    install_dir: Option<PathBuf>,
123    chain_id: &String,
124    id: AccountAddress,
125) -> Result<Option<AccountAddress>, anyhow::Error> {
126    let lock_file_path = package_path.join(SourcePackageLayout::Lock.path());
127    let Ok(mut lock_file) = File::open(lock_file_path.clone()) else {
128        return Ok(None);
129    };
130    let managed_package = ManagedPackage::read(&mut lock_file)
131        .ok()
132        .and_then(|m| m.into_iter().find(|(_, v)| v.chain_id == *chain_id));
133    let Some((env, v)) = managed_package else {
134        return Ok(None);
135    };
136    let install_dir = install_dir.unwrap_or(PathBuf::from("."));
137    let lock_for_update = LockFile::from(install_dir.clone(), &lock_file_path);
138    let Ok(mut lock_for_update) = lock_for_update else {
139        return Ok(None);
140    };
141    lock_file::schema::set_original_id(&mut lock_for_update, &env, &id.to_canonical_string(true))?;
142    lock_for_update.commit(lock_file_path)?;
143    let id = AccountAddress::from_str(&v.original_published_id)?;
144    Ok(Some(id))
145}
146
147/// Find the published on-chain ID in the `Move.lock` or `Move.toml` file.
148/// A chain ID of `None` means that we will only try to resolve a published ID
149/// from the Move.toml. The published ID is resolved from the `Move.toml` if the
150/// Move.lock does not exist. Else, we resolve from the `Move.lock`, where
151/// addresses are automatically managed. If conflicting IDs are found in the
152/// `Move.lock` vs. `Move.toml`, a "Conflict" error message returns.
153pub fn resolve_published_id(
154    package: &Package,
155    chain_id: Option<String>,
156) -> Result<ObjectID, PublishedAtError> {
157    // Look up a valid `published-at` in the `Move.toml` first, which we'll
158    // return if the Move.lock does not manage addresses.
159    let published_id_in_manifest = manifest_published_at(package);
160
161    match published_id_in_manifest {
162        Ok(_) | Err(PublishedAtError::NotPresent) => { /* nop */ }
163        Err(e) => {
164            return Err(e);
165        }
166    }
167
168    let lock = package.package_path.join(SourcePackageLayout::Lock.path());
169    let Ok(mut lock_file) = File::open(lock.clone()) else {
170        return published_id_in_manifest;
171    };
172
173    // Find the environment and ManagedPackage data for this chain_id.
174    let id_in_lock_for_chain_id =
175        lock_published_at(ManagedPackage::read(&mut lock_file).ok(), chain_id.as_ref());
176
177    match (id_in_lock_for_chain_id, published_id_in_manifest) {
178        (Ok(id_lock), Ok(id_manifest)) if id_lock != id_manifest => {
179            Err(PublishedAtError::Conflict {
180                id_lock,
181                id_manifest,
182            })
183        }
184
185        (Ok(id), _) | (_, Ok(id)) => Ok(id),
186
187        // We return early (above) if we failed to read the ID from the manifest for some reason
188        // other than it not being present, so at this point, we can defer to whatever error came
189        // from the lock file (Ok case is handled above).
190        (from_lock, Err(_)) => from_lock,
191    }
192}
193
194fn manifest_published_at(package: &Package) -> Result<ObjectID, PublishedAtError> {
195    let Some(value) = package
196        .source_package
197        .package
198        .custom_properties
199        .get(&Symbol::from(PUBLISHED_AT_MANIFEST_FIELD))
200    else {
201        return Err(PublishedAtError::NotPresent);
202    };
203
204    let id =
205        ObjectID::from_str(value.as_str()).map_err(|_| PublishedAtError::Invalid(value.clone()))?;
206
207    if id == ObjectID::ZERO {
208        Err(PublishedAtError::NotPresent)
209    } else {
210        Ok(id)
211    }
212}
213
214fn lock_published_at(
215    lock: Option<HashMap<String, ManagedPackage>>,
216    chain_id: Option<&String>,
217) -> Result<ObjectID, PublishedAtError> {
218    let (Some(lock), Some(chain_id)) = (lock, chain_id) else {
219        return Err(PublishedAtError::NotPresent);
220    };
221
222    let managed_package = lock
223        .into_values()
224        .find(|v| v.chain_id == *chain_id)
225        .ok_or(PublishedAtError::NotPresent)?;
226
227    let id = ObjectID::from_str(managed_package.latest_published_id.as_str())
228        .map_err(|_| PublishedAtError::Invalid(managed_package.latest_published_id.clone()))?;
229
230    if id == ObjectID::ZERO {
231        Err(PublishedAtError::NotPresent)
232    } else {
233        Ok(id)
234    }
235}