Radish alpha
r
Radicle desktop app
Radicle
Git (anonymous pull)
Log in to clone via SSH
Replace Error::Iroh catch-alls with typed variants
Daniel Norman committed 7 days ago
commit f83c9a6dea3727b25ed391caff0cfa11e94820ce
parent b6732bbac7fe95022e878c14d7017b0629247021
7 files changed +144 -153
modified crates/radicle-tauri/src/commands/cob/release.rs
@@ -17,9 +17,7 @@ pub async fn list_releases(
    rid: RepoId,
) -> Result<Vec<release::Release>, Error> {
    let ctx = ctx.inner().clone();
-
    tauri::async_runtime::spawn_blocking(move || ctx.list_releases(rid))
-
        .await
-
        .map_err(|e| Error::Iroh(format!("join: {e}")))?
+
    tauri::async_runtime::spawn_blocking(move || ctx.list_releases(rid)).await?
}

#[tauri::command]
@@ -29,9 +27,7 @@ pub async fn release_by_id(
    release_id: String,
) -> Result<Option<release::Release>, Error> {
    let ctx = ctx.inner().clone();
-
    tauri::async_runtime::spawn_blocking(move || ctx.release_by_id(rid, release_id))
-
        .await
-
        .map_err(|e| Error::Iroh(format!("join: {e}")))?
+
    tauri::async_runtime::spawn_blocking(move || ctx.release_by_id(rid, release_id)).await?
}

#[tauri::command]
@@ -41,9 +37,7 @@ pub async fn releases_by_commit(
    sha: git::Oid,
) -> Result<Vec<release::Release>, Error> {
    let ctx = ctx.inner().clone();
-
    tauri::async_runtime::spawn_blocking(move || ctx.releases_by_commit(rid, sha))
-
        .await
-
        .map_err(|e| Error::Iroh(format!("join: {e}")))?
+
    tauri::async_runtime::spawn_blocking(move || ctx.releases_by_commit(rid, sha)).await?
}

/// CID computation can be expensive on large files; off-load to the
@@ -54,9 +48,7 @@ pub async fn compute_artifact_cid(
    path: PathBuf,
) -> Result<String, Error> {
    let ctx = ctx.inner().clone();
-
    tauri::async_runtime::spawn_blocking(move || ctx.compute_cid(path))
-
        .await
-
        .map_err(|e| Error::Iroh(format!("join: {e}")))?
+
    tauri::async_runtime::spawn_blocking(move || ctx.compute_cid(path)).await?
}

#[tauri::command]
@@ -67,9 +59,7 @@ pub async fn create_or_open_release(
    tag: Option<git::Oid>,
) -> Result<String, Error> {
    let ctx = ctx.inner().clone();
-
    tauri::async_runtime::spawn_blocking(move || ctx.create_or_open_release(rid, oid, tag))
-
        .await
-
        .map_err(|e| Error::Iroh(format!("join: {e}")))?
+
    tauri::async_runtime::spawn_blocking(move || ctx.create_or_open_release(rid, oid, tag)).await?
}

#[tauri::command]
@@ -82,8 +72,7 @@ pub async fn add_artifact(
) -> Result<(), Error> {
    let ctx = ctx.inner().clone();
    tauri::async_runtime::spawn_blocking(move || ctx.add_artifact(rid, release_id, cid, name))
-
        .await
-
        .map_err(|e| Error::Iroh(format!("join: {e}")))?
+
        .await?
}

#[tauri::command]
@@ -96,8 +85,7 @@ pub async fn add_location(
) -> Result<(), Error> {
    let ctx = ctx.inner().clone();
    tauri::async_runtime::spawn_blocking(move || ctx.add_location(rid, release_id, cid, url))
-
        .await
-
        .map_err(|e| Error::Iroh(format!("join: {e}")))?
+
        .await?
}

#[tauri::command]
@@ -110,8 +98,7 @@ pub async fn remove_location(
) -> Result<(), Error> {
    let ctx = ctx.inner().clone();
    tauri::async_runtime::spawn_blocking(move || ctx.remove_location(rid, release_id, cid, url))
-
        .await
-
        .map_err(|e| Error::Iroh(format!("join: {e}")))?
+
        .await?
}

#[tauri::command]
@@ -122,9 +109,7 @@ pub async fn attest_artifact(
    cid: String,
) -> Result<(), Error> {
    let ctx = ctx.inner().clone();
-
    tauri::async_runtime::spawn_blocking(move || ctx.attest_artifact(rid, release_id, cid))
-
        .await
-
        .map_err(|e| Error::Iroh(format!("join: {e}")))?
+
    tauri::async_runtime::spawn_blocking(move || ctx.attest_artifact(rid, release_id, cid)).await?
}

#[tauri::command]
@@ -137,8 +122,7 @@ pub async fn redact_artifact(
) -> Result<(), Error> {
    let ctx = ctx.inner().clone();
    tauri::async_runtime::spawn_blocking(move || ctx.redact_artifact(rid, release_id, cid, reason))
-
        .await
-
        .map_err(|e| Error::Iroh(format!("join: {e}")))?
+
        .await?
}

// Seed / unseed -------------------------------------------------------------
@@ -163,8 +147,7 @@ pub async fn seed_artifact(
    let url_for_return = url.clone();
    let ctx_clone = ctx.inner().clone();
    tauri::async_runtime::spawn_blocking(move || ctx_clone.add_location(rid, release_id, cid, url))
-
        .await
-
        .map_err(|e| Error::Iroh(format!("join: {e}")))??;
+
        .await??;

    Ok(url_for_return)
}
@@ -186,8 +169,7 @@ pub async fn unseed_artifact(
    tauri::async_runtime::spawn_blocking(move || {
        ctx_clone.remove_location(rid, release_id_for_remove, cid_for_remove, url)
    })
-
    .await
-
    .map_err(|e| Error::Iroh(format!("join: {e}")))??;
+
    .await??;

    seeder::unseed(&iroh.blobs, &cid).await?;
    Ok(())
@@ -224,8 +206,7 @@ pub async fn download_artifact(
    let locations = tauri::async_runtime::spawn_blocking(move || {
        ctx_clone.artifact_locations(rid, release_id_for_locations, cid_for_locations)
    })
-
    .await
-
    .map_err(|e| Error::Iroh(format!("join: {e}")))??;
+
    .await??;

    let (parsed_cid, hash, kind) = fetch::cid_to_hash_str(&cid)?;

@@ -282,7 +263,7 @@ pub async fn pick_artifact_files(app: tauri::AppHandle) -> Result<Vec<String>, E
    app.dialog().file().pick_files(move |paths| {
        let _ = tx.send(paths.unwrap_or_default());
    });
-
    let paths = rx.await.map_err(|e| Error::Iroh(format!("dialog: {e}")))?;
+
    let paths = rx.await.map_err(|_| Error::DialogClosed)?;
    Ok(paths
        .into_iter()
        .filter_map(|p| p.into_path().ok())
@@ -298,7 +279,7 @@ pub async fn pick_artifact_directory(app: tauri::AppHandle) -> Result<Option<Str
    app.dialog().file().pick_folder(move |path| {
        let _ = tx.send(path);
    });
-
    let path = rx.await.map_err(|e| Error::Iroh(format!("dialog: {e}")))?;
+
    let path = rx.await.map_err(|_| Error::DialogClosed)?;
    Ok(path
        .and_then(|p| p.into_path().ok())
        .map(|p| p.to_string_lossy().into_owned()))
modified crates/radicle-tauri/src/commands/repo.rs
@@ -127,9 +127,7 @@ pub async fn list_tags(
    rid: RepoId,
) -> Result<Vec<types::repo::Tag>, Error> {
    let ctx = ctx.inner().clone();
-
    tauri::async_runtime::spawn_blocking(move || ctx.list_tags(rid))
-
        .await
-
        .map_err(|e| Error::Iroh(format!("join: {e}")))?
+
    tauri::async_runtime::spawn_blocking(move || ctx.list_tags(rid)).await?
}

#[tauri::command]
modified crates/radicle-types/src/error.rs
@@ -171,6 +171,54 @@ pub enum Error {
    /// Iroh / iroh-blobs error.
    #[error("iroh: {0}")]
    Iroh(String),
+

+
    /// Release creation error.
+
    #[error(transparent)]
+
    ReleaseCreate(#[from] radicle_artifact::error::Create),
+

+
    /// Release redaction error.
+
    #[error(transparent)]
+
    ReleaseRedact(#[from] radicle_artifact::error::Redact),
+

+
    /// Artifact share error (CID parsing, hashing, content addressing).
+
    #[error(transparent)]
+
    ArtifactShare(#[from] radicle_artifact::share::Error),
+

+
    /// CID parse error.
+
    #[error(transparent)]
+
    Cid(#[from] cid::Error),
+

+
    /// URL parse error.
+
    #[error(transparent)]
+
    Url(#[from] url::ParseError),
+

+
    /// COB object id parse error.
+
    #[error(transparent)]
+
    ParseObjectId(#[from] radicle::cob::object::ParseObjectId),
+

+
    /// File picker / dialog closed before returning a result.
+
    #[error("dialog was closed before returning a result")]
+
    DialogClosed,
+

+
    /// No iroh providers reachable for the requested CID.
+
    #[error("no iroh providers reachable for {cid}")]
+
    NoIrohProviders { cid: String },
+

+
    /// Release with the given id was not found in the COB store.
+
    #[error("release {release_id} not found")]
+
    ReleaseNotFound { release_id: String },
+

+
    /// Artifact CID is not registered against the given release.
+
    #[error("artifact {cid} not in release {release_id}")]
+
    ArtifactNotInRelease { cid: String, release_id: String },
+

+
    /// Persisted iroh secret key file does not contain 32 bytes.
+
    #[error("malformed iroh key at {path}")]
+
    MalformedIrohKey { path: std::path::PathBuf },
+

+
    /// CID computed from imported content does not match the expected CID.
+
    #[error("cid mismatch: expected {expected}, got {actual}")]
+
    CidMismatch { expected: String, actual: String },
}

impl Error {
modified crates/radicle-types/src/fetch.rs
@@ -36,8 +36,8 @@ pub async fn fetch_artifact<F>(
where
    F: FnMut(FetchStage),
{
-
    let kind = cid_utils::artifact_kind(cid).map_err(|e| Error::Iroh(format!("cid: {e}")))?;
-
    let hash = cid_utils::cid_to_blake3_hash(cid).map_err(|e| Error::Iroh(format!("cid: {e}")))?;
+
    let kind = cid_utils::artifact_kind(cid)?;
+
    let hash = cid_utils::cid_to_blake3_hash(cid)?;
    let format = match kind {
        cid_utils::ArtifactKind::Blob => BlobFormat::Raw,
        cid_utils::ArtifactKind::Collection => BlobFormat::HashSeq,
@@ -45,9 +45,9 @@ where

    let providers = resolve_iroh_providers(locations);
    if providers.is_empty() {
-
        return Err(Error::Iroh(format!(
-
            "no iroh providers reachable for {cid}"
-
        )));
+
        return Err(Error::NoIrohProviders {
+
            cid: cid.to_string(),
+
        });
    }

    let downloader = Downloader::new(store, endpoint);
@@ -77,15 +77,15 @@ where
/// the caller needs both the format (for fetch / export) and the kind
/// (for branching blob vs collection).
pub fn cid_to_hash(cid: &Cid) -> Result<(Hash, cid_utils::ArtifactKind), Error> {
-
    let kind = cid_utils::artifact_kind(cid).map_err(|e| Error::Iroh(format!("cid: {e}")))?;
-
    let hash = cid_utils::cid_to_blake3_hash(cid).map_err(|e| Error::Iroh(format!("cid: {e}")))?;
+
    let kind = cid_utils::artifact_kind(cid)?;
+
    let hash = cid_utils::cid_to_blake3_hash(cid)?;
    Ok((hash, kind))
}

/// String-taking variant for callers (Tauri commands) that don't pull in
/// the `cid` crate directly.
pub fn cid_to_hash_str(cid: &str) -> Result<(Cid, Hash, cid_utils::ArtifactKind), Error> {
-
    let parsed = Cid::from_str(cid).map_err(|e| Error::Iroh(format!("invalid cid: {e}")))?;
+
    let parsed = Cid::from_str(cid)?;
    let (hash, kind) = cid_to_hash(&parsed)?;
    Ok((parsed, hash, kind))
}
modified crates/radicle-types/src/seeder.rs
@@ -56,7 +56,7 @@ fn load_or_generate_key(path: &PathBuf) -> Result<iroh::SecretKey, Error> {
        let bytes = std::fs::read(path)?;
        let bytes: [u8; 32] = bytes
            .try_into()
-
            .map_err(|_| Error::Iroh(format!("malformed iroh key at {}", path.display())))?;
+
            .map_err(|_| Error::MalformedIrohKey { path: path.clone() })?;
        Ok(iroh::SecretKey::from_bytes(&bytes))
    } else {
        let secret = iroh::SecretKey::generate();
@@ -106,17 +106,17 @@ pub async fn import_blob(store: &Store, path: &Path, expected: &Cid) -> Result<H

    let actual = cid_utils::blake3_hash_to_cid(tag.hash, ArtifactKind::Blob);
    if actual != *expected {
-
        return Err(Error::Iroh(format!(
-
            "cid mismatch: expected {expected}, got {actual}"
-
        )));
+
        return Err(Error::CidMismatch {
+
            expected: expected.to_string(),
+
            actual: actual.to_string(),
+
        });
    }
    Ok(tag.hash)
}

/// Import a directory as a Collection, copying each file's bytes.
pub async fn import_collection(store: &Store, dir: &Path, expected: &Cid) -> Result<Hash, Error> {
-
    let entries =
-
        cid_utils::canonical_walk(dir).map_err(|e| Error::Iroh(format!("walk directory: {e}")))?;
+
    let entries = cid_utils::canonical_walk(dir)?;

    let mut pairs: Vec<(String, Hash)> = Vec::new();
    for (name, abs) in entries {
@@ -140,9 +140,10 @@ pub async fn import_collection(store: &Store, dir: &Path, expected: &Cid) -> Res

    let actual = cid_utils::blake3_hash_to_cid(root_tag.hash(), ArtifactKind::Collection);
    if actual != *expected {
-
        return Err(Error::Iroh(format!(
-
            "cid mismatch: expected {expected}, got {actual}"
-
        )));
+
        return Err(Error::CidMismatch {
+
            expected: expected.to_string(),
+
            actual: actual.to_string(),
+
        });
    }
    Ok(root_tag.hash())
}
@@ -150,7 +151,7 @@ pub async fn import_collection(store: &Store, dir: &Path, expected: &Cid) -> Res
/// Mark a CID as seeded by setting the `seeded/{cid}` tag pointing at the
/// blob hash with the format appropriate for its kind (raw vs hash-seq).
pub async fn register_seeded(store: &Store, cid: &Cid, hash: Hash) -> Result<(), Error> {
-
    let kind = cid_utils::artifact_kind(cid).map_err(|e| Error::Iroh(format!("cid kind: {e}")))?;
+
    let kind = cid_utils::artifact_kind(cid)?;
    let value = match kind {
        ArtifactKind::Blob => HashAndFormat::raw(hash),
        ArtifactKind::Collection => HashAndFormat::hash_seq(hash),
@@ -185,9 +186,8 @@ pub async fn is_seeded(store: &Store, cid: &Cid) -> Result<bool, Error> {
/// the `cid` crate. Parses the CID, dispatches to the right import, sets
/// the seeded tag, and returns the import hash as a string for logging.
pub async fn seed(store: &Store, cid: &str, source: &Path) -> Result<(), Error> {
-
    let parsed_cid = Cid::from_str(cid).map_err(|e| Error::Iroh(format!("invalid cid: {e}")))?;
-
    let kind =
-
        cid_utils::artifact_kind(&parsed_cid).map_err(|e| Error::Iroh(format!("cid kind: {e}")))?;
+
    let parsed_cid = Cid::from_str(cid)?;
+
    let kind = cid_utils::artifact_kind(&parsed_cid)?;
    let hash = match kind {
        ArtifactKind::Blob => import_blob(store, source, &parsed_cid).await?,
        ArtifactKind::Collection => import_collection(store, source, &parsed_cid).await?,
@@ -197,12 +197,12 @@ pub async fn seed(store: &Store, cid: &str, source: &Path) -> Result<(), Error>
}

pub async fn unseed(store: &Store, cid: &str) -> Result<(), Error> {
-
    let parsed_cid = Cid::from_str(cid).map_err(|e| Error::Iroh(format!("invalid cid: {e}")))?;
+
    let parsed_cid = Cid::from_str(cid)?;
    unregister_seeded(store, &parsed_cid).await
}

pub async fn is_seeded_str(store: &Store, cid: &str) -> Result<bool, Error> {
-
    let parsed_cid = Cid::from_str(cid).map_err(|e| Error::Iroh(format!("invalid cid: {e}")))?;
+
    let parsed_cid = Cid::from_str(cid)?;
    is_seeded(store, &parsed_cid).await
}

modified crates/radicle-types/src/traits/release.rs
@@ -19,11 +19,11 @@ pub trait Releases: Profile {
        let aliases = profile.aliases();
        let our_did = identity::Did::from(profile.public_key);

-
        let releases = ReleasesStore::open(&repo).map_err(|e| Error::Iroh(e.to_string()))?;
+
        let releases = ReleasesStore::open(&repo)?;

        let mut out = Vec::new();
-
        for item in releases.all().map_err(|e| Error::Iroh(e.to_string()))? {
-
            let (id, release) = item.map_err(|e| Error::Iroh(e.to_string()))?;
+
        for item in releases.all()? {
+
            let (id, release) = item?;
            out.push(release::Release::new(
                radicle_artifact::ReleaseId::from(id),
                &release,
@@ -44,10 +44,9 @@ pub trait Releases: Profile {
        let aliases = profile.aliases();
        let our_did = identity::Did::from(profile.public_key);

-
        let id = radicle_artifact::ReleaseId::from_str(&release_id)
-
            .map_err(|e| Error::Iroh(format!("invalid release id: {e}")))?;
-
        let releases = ReleasesStore::open(&repo).map_err(|e| Error::Iroh(e.to_string()))?;
-
        let Some(release) = releases.get(&id).map_err(|e| Error::Iroh(e.to_string()))? else {
+
        let id = radicle_artifact::ReleaseId::from_str(&release_id)?;
+
        let releases = ReleasesStore::open(&repo)?;
+
        let Some(release) = releases.get(&id)? else {
            return Ok(None);
        };

@@ -66,14 +65,11 @@ pub trait Releases: Profile {
        let aliases = profile.aliases();
        let our_did = identity::Did::from(profile.public_key);

-
        let releases = ReleasesStore::open(&repo).map_err(|e| Error::Iroh(e.to_string()))?;
+
        let releases = ReleasesStore::open(&repo)?;

        let mut out = Vec::new();
-
        for item in releases
-
            .find_by_commit(oid)
-
            .map_err(|e| Error::Iroh(e.to_string()))?
-
        {
-
            let (id, release) = item.map_err(|e| Error::Iroh(e.to_string()))?;
+
        for item in releases.find_by_commit(oid)? {
+
            let (id, release) = item?;
            out.push(release::Release::new(id, &release, &our_did, &aliases));
        }
        Ok(out)
@@ -83,9 +79,9 @@ pub trait Releases: Profile {
    /// Files become Blob CIDs, directories become Collection CIDs.
    fn compute_cid(&self, path: PathBuf) -> Result<String, Error> {
        let cid = if path.is_dir() {
-
            cid_utils::compute_content_id(&path).map_err(|e| Error::Iroh(e.to_string()))?
+
            cid_utils::compute_content_id(&path)?
        } else {
-
            cid_utils::compute_blob_cid(&path).map_err(|e| Error::Iroh(e.to_string()))?
+
            cid_utils::compute_blob_cid(&path)?
        };
        Ok(cid.to_string())
    }
@@ -103,19 +99,20 @@ pub trait Releases: Profile {
        let profile = self.profile();
        let repo = profile.storage.repository(rid)?;

-
        let id = radicle_artifact::ReleaseId::from_str(&release_id)
-
            .map_err(|e| Error::Iroh(format!("invalid release id: {e}")))?;
-
        let parsed_cid =
-
            cid::Cid::from_str(&cid).map_err(|e| Error::Iroh(format!("invalid cid: {e}")))?;
+
        let id = radicle_artifact::ReleaseId::from_str(&release_id)?;
+
        let parsed_cid = cid::Cid::from_str(&cid)?;

-
        let releases = ReleasesStore::open(&repo).map_err(|e| Error::Iroh(e.to_string()))?;
-
        let release = releases
-
            .get(&id)
-
            .map_err(|e| Error::Iroh(e.to_string()))?
-
            .ok_or_else(|| Error::Iroh(format!("release {release_id} not found")))?;
-
        let artifact = release
-
            .artifact(&parsed_cid)
-
            .ok_or_else(|| Error::Iroh(format!("artifact {cid} not in release {release_id}")))?;
+
        let releases = ReleasesStore::open(&repo)?;
+
        let release = releases.get(&id)?.ok_or_else(|| Error::ReleaseNotFound {
+
            release_id: release_id.clone(),
+
        })?;
+
        let artifact =
+
            release
+
                .artifact(&parsed_cid)
+
                .ok_or_else(|| Error::ArtifactNotInRelease {
+
                    cid: cid.clone(),
+
                    release_id: release_id.clone(),
+
                })?;
        Ok(artifact.locations().clone())
    }
}
modified crates/radicle-types/src/traits/release_mut.rs
@@ -29,23 +29,19 @@ pub trait ReleasesMut: Releases {
        let signer = profile.signer()?;
        let repo = profile.storage.repository(rid)?;

-
        let mut releases = ReleasesStore::open(&repo).map_err(|e| Error::Iroh(e.to_string()))?;
+
        let mut releases = ReleasesStore::open(&repo)?;

        let existing = {
-
            let mut iter = releases
-
                .find_by_commit(oid)
-
                .map_err(|e| Error::Iroh(e.to_string()))?;
+
            let mut iter = releases.find_by_commit(oid)?;
            match iter.next() {
-
                Some(item) => Some(item.map_err(|e| Error::Iroh(e.to_string()))?.0),
+
                Some(item) => Some(item?.0),
                None => None,
            }
        };
        let id = match existing {
            Some(id) => id,
            None => {
-
                let release = releases
-
                    .create(oid, tag, &signer)
-
                    .map_err(|e| Error::Iroh(e.to_string()))?;
+
                let release = releases.create(oid, tag, &signer)?;
                *release.id()
            }
        };
@@ -63,16 +59,12 @@ pub trait ReleasesMut: Releases {
        let signer = profile.signer()?;
        let repo = profile.storage.repository(rid)?;

-
        let id = parse_release_id(&release_id)?;
-
        let cid = parse_cid(&cid)?;
+
        let id = radicle_artifact::ReleaseId::from_str(&release_id)?;
+
        let cid = cid::Cid::from_str(&cid)?;

-
        let mut releases = ReleasesStore::open(&repo).map_err(|e| Error::Iroh(e.to_string()))?;
-
        let mut release = releases
-
            .get_mut(&id)
-
            .map_err(|e| Error::Iroh(e.to_string()))?;
-
        release
-
            .add_artifact(cid, name, &signer)
-
            .map_err(|e| Error::Iroh(e.to_string()))?;
+
        let mut releases = ReleasesStore::open(&repo)?;
+
        let mut release = releases.get_mut(&id)?;
+
        release.add_artifact(cid, name, &signer)?;
        Ok(())
    }

@@ -87,17 +79,13 @@ pub trait ReleasesMut: Releases {
        let signer = profile.signer()?;
        let repo = profile.storage.repository(rid)?;

-
        let id = parse_release_id(&release_id)?;
-
        let cid = parse_cid(&cid)?;
-
        let url = Url::parse(&url).map_err(|e| Error::Iroh(format!("invalid url: {e}")))?;
-

-
        let mut releases = ReleasesStore::open(&repo).map_err(|e| Error::Iroh(e.to_string()))?;
-
        let mut release = releases
-
            .get_mut(&id)
-
            .map_err(|e| Error::Iroh(e.to_string()))?;
-
        release
-
            .add_location(cid, url, &signer)
-
            .map_err(|e| Error::Iroh(e.to_string()))?;
+
        let id = radicle_artifact::ReleaseId::from_str(&release_id)?;
+
        let cid = cid::Cid::from_str(&cid)?;
+
        let url = Url::parse(&url)?;
+

+
        let mut releases = ReleasesStore::open(&repo)?;
+
        let mut release = releases.get_mut(&id)?;
+
        release.add_location(cid, url, &signer)?;
        Ok(())
    }

@@ -112,17 +100,13 @@ pub trait ReleasesMut: Releases {
        let signer = profile.signer()?;
        let repo = profile.storage.repository(rid)?;

-
        let id = parse_release_id(&release_id)?;
-
        let cid = parse_cid(&cid)?;
-
        let url = Url::parse(&url).map_err(|e| Error::Iroh(format!("invalid url: {e}")))?;
-

-
        let mut releases = ReleasesStore::open(&repo).map_err(|e| Error::Iroh(e.to_string()))?;
-
        let mut release = releases
-
            .get_mut(&id)
-
            .map_err(|e| Error::Iroh(e.to_string()))?;
-
        release
-
            .remove_location(cid, url, &signer)
-
            .map_err(|e| Error::Iroh(e.to_string()))?;
+
        let id = radicle_artifact::ReleaseId::from_str(&release_id)?;
+
        let cid = cid::Cid::from_str(&cid)?;
+
        let url = Url::parse(&url)?;
+

+
        let mut releases = ReleasesStore::open(&repo)?;
+
        let mut release = releases.get_mut(&id)?;
+
        release.remove_location(cid, url, &signer)?;
        Ok(())
    }

@@ -136,16 +120,12 @@ pub trait ReleasesMut: Releases {
        let signer = profile.signer()?;
        let repo = profile.storage.repository(rid)?;

-
        let id = parse_release_id(&release_id)?;
-
        let cid = parse_cid(&cid)?;
+
        let id = radicle_artifact::ReleaseId::from_str(&release_id)?;
+
        let cid = cid::Cid::from_str(&cid)?;

-
        let mut releases = ReleasesStore::open(&repo).map_err(|e| Error::Iroh(e.to_string()))?;
-
        let mut release = releases
-
            .get_mut(&id)
-
            .map_err(|e| Error::Iroh(e.to_string()))?;
-
        release
-
            .attest(cid, &signer)
-
            .map_err(|e| Error::Iroh(e.to_string()))?;
+
        let mut releases = ReleasesStore::open(&repo)?;
+
        let mut release = releases.get_mut(&id)?;
+
        release.attest(cid, &signer)?;
        Ok(())
    }

@@ -160,25 +140,12 @@ pub trait ReleasesMut: Releases {
        let signer = profile.signer()?;
        let repo = profile.storage.repository(rid)?;

-
        let id = parse_release_id(&release_id)?;
-
        let cid = parse_cid(&cid)?;
+
        let id = radicle_artifact::ReleaseId::from_str(&release_id)?;
+
        let cid = cid::Cid::from_str(&cid)?;

-
        let mut releases = ReleasesStore::open(&repo).map_err(|e| Error::Iroh(e.to_string()))?;
-
        let mut release = releases
-
            .get_mut(&id)
-
            .map_err(|e| Error::Iroh(e.to_string()))?;
-
        release
-
            .redact(cid, reason, &signer)
-
            .map_err(|e| Error::Iroh(e.to_string()))?;
+
        let mut releases = ReleasesStore::open(&repo)?;
+
        let mut release = releases.get_mut(&id)?;
+
        release.redact(cid, reason, &signer)?;
        Ok(())
    }
}
-

-
fn parse_release_id(s: &str) -> Result<radicle_artifact::ReleaseId, Error> {
-
    radicle_artifact::ReleaseId::from_str(s)
-
        .map_err(|e| Error::Iroh(format!("invalid release id {s}: {e}")))
-
}
-

-
fn parse_cid(s: &str) -> Result<cid::Cid, Error> {
-
    cid::Cid::from_str(s).map_err(|e| Error::Iroh(format!("invalid cid {s}: {e}")))
-
}