sui_package_management/
lib.rs

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