Skip to main content

cow_app_data/
ipfs.rs

1//! `MetadataApi` facade and IPFS fetch/upload helpers for `CoW` Protocol
2//! app-data.
3//!
4//! This module provides two layers of API:
5//!
6//! 1. **Free functions** — stateless helpers like [`get_app_data_info`], [`validate_app_data_doc`],
7//!    [`fetch_doc_from_cid`], and [`upload_app_data_to_pinata`] that operate on explicit
8//!    parameters.
9//!
10//! 2. **[`MetadataApi`]** — an ergonomic facade that bundles an [`Ipfs`] configuration and
11//!    delegates to the free functions, mirroring the `MetadataApi` class from the `TypeScript` SDK.
12//!
13//! Most users will interact through [`MetadataApi`]:
14//!
15//! ```rust
16//! use cow_app_data::{AppDataDoc, MetadataApi};
17//!
18//! let api = MetadataApi::new();
19//! let doc = api.generate_app_data_doc("MyApp");
20//! let info = api.get_app_data_info(&doc).unwrap();
21//! println!("appData hex : {}", info.app_data_hex);
22//! println!("CID         : {}", info.cid);
23//! ```
24
25use std::fmt;
26
27use alloy_primitives::B256;
28use serde::Deserialize;
29use serde_json::json;
30
31use cow_errors::CowError;
32
33use super::{
34    cid::{appdata_hex_to_cid, cid_to_appdata_hex, extract_digest},
35    hash::{appdata_hex, stringify_deterministic},
36    types::{AppDataDoc, Metadata},
37    validation::{ValidationError, validate_constraints},
38};
39
40/// Default IPFS gateway used when none is provided.
41pub const DEFAULT_IPFS_READ_URI: &str = "https://cloudflare-ipfs.com/ipfs";
42
43/// Default IPFS write URI (Pinata).
44pub const DEFAULT_IPFS_WRITE_URI: &str = "https://api.pinata.cloud";
45
46// ── Extra types ───────────────────────────────────────────────────────────────
47
48/// Full app-data information derived from an [`AppDataDoc`].
49///
50/// Bundles the three representations of an order's app-data that are
51/// needed at different stages of the order lifecycle:
52///
53/// - **`cid`** — used to store/retrieve the document on IPFS.
54/// - **`app_data_content`** — the canonical JSON whose `keccak256` equals `app_data_hex`. Pin this
55///   string on IPFS so solvers can read the metadata.
56/// - **`app_data_hex`** — the 32-byte value placed in the on-chain order struct.
57///
58/// Obtain an instance via [`get_app_data_info`] or [`MetadataApi::get_app_data_info`].
59///
60/// # Example
61///
62/// ```
63/// use cow_app_data::{AppDataDoc, get_app_data_info};
64///
65/// let doc = AppDataDoc::new("MyDApp");
66/// let info = get_app_data_info(&doc).unwrap();
67/// assert!(info.app_data_hex.starts_with("0x"));
68/// assert!(info.cid.starts_with('f'));
69/// assert!(info.app_data_content.contains("MyDApp"));
70/// ```
71#[derive(Debug, Clone)]
72pub struct AppDataInfo {
73    /// IPFS `CIDv1` string for the order's app-data.
74    pub cid: String,
75    /// Canonical JSON string whose `keccak256` equals [`Self::app_data_hex`].
76    pub app_data_content: String,
77    /// `0x`-prefixed 32-byte hex used as `appData` in the on-chain order struct.
78    pub app_data_hex: String,
79}
80
81impl AppDataInfo {
82    /// Construct an [`AppDataInfo`] from its three constituent fields.
83    ///
84    /// Prefer [`get_app_data_info`] to derive all three values from an
85    /// [`AppDataDoc`] automatically. Use this constructor only when you
86    /// already have the CID, JSON content, and hex hash from an external
87    /// source.
88    ///
89    /// # Parameters
90    ///
91    /// * `cid` — the IPFS `CIDv1` base16 string.
92    /// * `app_data_content` — the canonical JSON string.
93    /// * `app_data_hex` — the `0x`-prefixed 32-byte `keccak256` hex.
94    ///
95    /// # Returns
96    ///
97    /// A new [`AppDataInfo`] instance.
98    #[must_use]
99    pub fn new(
100        cid: impl Into<String>,
101        app_data_content: impl Into<String>,
102        app_data_hex: impl Into<String>,
103    ) -> Self {
104        Self {
105            cid: cid.into(),
106            app_data_content: app_data_content.into(),
107            app_data_hex: app_data_hex.into(),
108        }
109    }
110}
111
112impl fmt::Display for AppDataInfo {
113    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
114        write!(f, "app-data-info({}, {})", self.cid, self.app_data_hex)
115    }
116}
117
118/// IPFS connection parameters for upload/fetch operations.
119///
120/// Configure read/write gateway URIs and optional Pinata API credentials.
121/// Pass an instance to [`MetadataApi::with_ipfs`] or directly to
122/// [`upload_app_data_to_pinata`].
123///
124/// # Example
125///
126/// ```
127/// use cow_app_data::Ipfs;
128///
129/// let ipfs = Ipfs::default()
130///     .with_read_uri("https://my-gateway.io/ipfs")
131///     .with_pinata("my-api-key", "my-api-secret");
132/// assert_eq!(ipfs.read_uri.as_deref(), Some("https://my-gateway.io/ipfs"));
133/// ```
134#[derive(Debug, Clone, Default)]
135pub struct Ipfs {
136    /// IPFS read gateway URI (defaults to [`DEFAULT_IPFS_READ_URI`]).
137    pub read_uri: Option<String>,
138    /// IPFS write gateway URI (defaults to [`DEFAULT_IPFS_WRITE_URI`]).
139    pub write_uri: Option<String>,
140    /// Pinata API key for authenticated uploads.
141    pub pinata_api_key: Option<String>,
142    /// Pinata API secret for authenticated uploads.
143    pub pinata_api_secret: Option<String>,
144}
145
146/// Result of validating an [`AppDataDoc`] against its schema.
147///
148/// Contains both human-readable error strings (for logging / display) and
149/// typed [`ValidationError`] values (for programmatic inspection). An empty
150/// [`typed_errors`](Self::typed_errors) list means the document is valid.
151///
152/// Obtain an instance via [`validate_app_data_doc`] or
153/// [`MetadataApi::validate_app_data_doc`].
154///
155/// # Example
156///
157/// ```
158/// use cow_app_data::{AppDataDoc, validate_app_data_doc};
159///
160/// let result = validate_app_data_doc(&AppDataDoc::new("OK"));
161/// assert!(result.is_valid());
162/// assert!(!result.has_errors());
163/// assert_eq!(result.error_count(), 0);
164/// ```
165#[derive(Debug, Clone)]
166pub struct ValidationResult {
167    /// Whether the document is valid (no errors found).
168    pub success: bool,
169    /// Human-readable validation errors (empty when `success` is true).
170    ///
171    /// Kept as `Vec<String>` for backwards compatibility with callers that
172    /// only inspect the string messages; typed errors are in [`Self::typed_errors`].
173    pub errors: Vec<String>,
174    /// Structured, typed constraint violations (empty when `success` is true).
175    pub typed_errors: Vec<ValidationError>,
176}
177
178impl ValidationResult {
179    /// Construct a [`ValidationResult`] from a success flag and
180    /// human-readable error list.
181    ///
182    /// The `typed_errors` field is initialised to an empty `Vec`. Callers
183    /// typically use [`validate_app_data_doc`] instead, which populates
184    /// both the string errors and typed errors automatically.
185    ///
186    /// # Parameters
187    ///
188    /// * `success` — whether the document is valid.
189    /// * `errors` — human-readable error messages.
190    ///
191    /// # Returns
192    ///
193    /// A new [`ValidationResult`] with an empty `typed_errors` list.
194    #[must_use]
195    pub const fn new(success: bool, errors: Vec<String>) -> Self {
196        Self { success, errors, typed_errors: Vec::new() }
197    }
198
199    /// Returns `true` when validation succeeded (no errors).
200    ///
201    /// Equivalent to checking `typed_errors.is_empty()`, but stored as a
202    /// precomputed flag for convenience.
203    #[must_use]
204    pub const fn is_valid(&self) -> bool {
205        self.success
206    }
207
208    /// Returns `true` when at least one constraint violation was found.
209    ///
210    /// The inverse of [`is_valid`](Self::is_valid).
211    #[must_use]
212    pub const fn has_errors(&self) -> bool {
213        !self.typed_errors.is_empty()
214    }
215
216    /// Returns the number of typed constraint violations.
217    ///
218    /// # Returns
219    ///
220    /// `0` when the document is valid, `> 0` otherwise.
221    #[must_use]
222    pub const fn error_count(&self) -> usize {
223        self.typed_errors.len()
224    }
225
226    /// Returns a slice of all typed constraint violations.
227    ///
228    /// Use this for programmatic inspection of validation errors. Each
229    /// [`ValidationError`] variant carries enough context to build a
230    /// diagnostic message.
231    ///
232    /// # Returns
233    ///
234    /// An empty slice when the document is valid.
235    #[must_use]
236    pub fn errors_ref(&self) -> &[ValidationError] {
237        &self.typed_errors
238    }
239
240    /// Returns the first typed constraint violation, if any.
241    ///
242    /// Useful for quick "fail on first error" workflows.
243    ///
244    /// # Returns
245    ///
246    /// `None` when the document is valid, `Some(&error)` otherwise.
247    #[must_use]
248    pub fn first_error(&self) -> Option<&ValidationError> {
249        self.typed_errors.first()
250    }
251}
252
253impl fmt::Display for ValidationResult {
254    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
255        if self.success {
256            f.write_str("valid")
257        } else {
258            write!(f, "invalid({} errors)", self.typed_errors.len())
259        }
260    }
261}
262
263impl Ipfs {
264    /// Set the IPFS read gateway URI.
265    ///
266    /// Overrides the default [`DEFAULT_IPFS_READ_URI`] (`cloudflare-ipfs.com`)
267    /// for all fetch operations.
268    ///
269    /// # Parameters
270    ///
271    /// * `uri` — the base URL of the IPFS read gateway (e.g. `"https://my-gateway.io/ipfs"`).
272    ///
273    /// # Returns
274    ///
275    /// `self` with `read_uri` set.
276    #[must_use]
277    pub fn with_read_uri(mut self, uri: impl Into<String>) -> Self {
278        self.read_uri = Some(uri.into());
279        self
280    }
281
282    /// Set the IPFS write gateway URI.
283    ///
284    /// Overrides the default [`DEFAULT_IPFS_WRITE_URI`] (`api.pinata.cloud`)
285    /// for all upload operations.
286    ///
287    /// # Parameters
288    ///
289    /// * `uri` — the base URL of the IPFS write gateway.
290    ///
291    /// # Returns
292    ///
293    /// `self` with `write_uri` set.
294    #[must_use]
295    pub fn with_write_uri(mut self, uri: impl Into<String>) -> Self {
296        self.write_uri = Some(uri.into());
297        self
298    }
299
300    /// Set Pinata API credentials for authenticated uploads.
301    ///
302    /// Both the API key and secret are required for
303    /// [`upload_app_data_to_pinata`] to succeed. Obtain them from the Pinata
304    /// dashboard.
305    ///
306    /// # Parameters
307    ///
308    /// * `api_key` — your Pinata API key.
309    /// * `api_secret` — your Pinata API secret.
310    ///
311    /// # Returns
312    ///
313    /// `self` with both `pinata_api_key` and `pinata_api_secret` set.
314    #[must_use]
315    pub fn with_pinata(
316        mut self,
317        api_key: impl Into<String>,
318        api_secret: impl Into<String>,
319    ) -> Self {
320        self.pinata_api_key = Some(api_key.into());
321        self.pinata_api_secret = Some(api_secret.into());
322        self
323    }
324}
325
326impl fmt::Display for Ipfs {
327    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
328        let uri = self.read_uri.as_deref().map_or("default", |s| s);
329        write!(f, "ipfs(read={uri})")
330    }
331}
332
333// ── AppDataInfo helpers ───────────────────────────────────────────────────────
334
335/// Derive the full [`AppDataInfo`] from a document.
336///
337/// Performs three steps in sequence:
338/// 1. Serialise `doc` to deterministic JSON via [`stringify_deterministic`].
339/// 2. Compute `keccak256(json_bytes)` to get the `appData` hex.
340/// 3. Convert the hex to a `CIDv1` string via [`appdata_hex_to_cid`].
341///
342/// Mirrors `getAppDataInfo` from the `@cowprotocol/app-data` `TypeScript`
343/// package.
344///
345/// # Parameters
346///
347/// * `doc` — the [`AppDataDoc`] to derive info from.
348///
349/// # Returns
350///
351/// An [`AppDataInfo`] containing the canonical JSON, `0x`-prefixed hex
352/// hash, and base16 `CIDv1` string.
353///
354/// # Errors
355///
356/// Returns [`CowError::AppData`] on serialisation or CID conversion failure.
357///
358/// # Example
359///
360/// ```
361/// use cow_app_data::{AppDataDoc, get_app_data_info};
362///
363/// let doc = AppDataDoc::new("CoW Swap");
364/// let info = get_app_data_info(&doc)?;
365/// assert!(!info.cid.is_empty());
366/// assert!(info.app_data_hex.starts_with("0x"));
367/// assert!(!info.app_data_content.is_empty());
368/// # Ok::<(), cow_errors::CowError>(())
369/// ```
370pub fn get_app_data_info(doc: &AppDataDoc) -> Result<AppDataInfo, CowError> {
371    let app_data_content = stringify_deterministic(doc)?;
372    let hash: B256 = alloy_primitives::keccak256(app_data_content.as_bytes());
373    let app_data_hex = format!("0x{}", alloy_primitives::hex::encode(hash.as_slice()));
374    let cid = appdata_hex_to_cid(&app_data_hex)?;
375    Ok(AppDataInfo { cid, app_data_content, app_data_hex })
376}
377
378/// Derive [`AppDataInfo`] from a pre-serialised app-data JSON string.
379///
380/// Unlike [`get_app_data_info`], this function does **not** re-serialise
381/// the document — it treats `json` as the canonical pre-image and hashes
382/// it directly with `keccak256`. Use this when you have a string that was
383/// already produced by [`stringify_deterministic`] and must not be
384/// re-encoded, or when you received a JSON string from an external source
385/// and want to compute its on-chain hash.
386///
387/// # Parameters
388///
389/// * `json` — the canonical JSON string to hash.
390///
391/// # Returns
392///
393/// An [`AppDataInfo`] where `app_data_content` is `json` verbatim.
394///
395/// # Errors
396///
397/// Returns [`CowError::AppData`] on `CID` conversion failure.
398///
399/// # Example
400///
401/// ```
402/// use cow_app_data::{AppDataDoc, MetadataApi, stringify_deterministic};
403///
404/// let doc = AppDataDoc::new("CoW Swap");
405/// let canonical_json = stringify_deterministic(&doc)?;
406/// let api = MetadataApi::new();
407/// let info = api.get_app_data_info_from_str(&canonical_json)?;
408/// assert!(info.app_data_hex.starts_with("0x"));
409/// assert_eq!(info.app_data_content, canonical_json);
410/// # Ok::<(), cow_errors::CowError>(())
411/// ```
412pub fn get_app_data_info_from_str(json: &str) -> Result<AppDataInfo, CowError> {
413    let hash: alloy_primitives::B256 = alloy_primitives::keccak256(json.as_bytes());
414    let app_data_hex = format!("0x{}", alloy_primitives::hex::encode(hash.as_slice()));
415    let cid = appdata_hex_to_cid(&app_data_hex)?;
416    Ok(AppDataInfo { cid, app_data_content: json.to_owned(), app_data_hex })
417}
418
419/// Validate an [`AppDataDoc`] against all `CoW` Protocol app-data rules.
420///
421/// Runs up to three independent checks and merges their results into a
422/// single [`ValidationResult`]:
423///
424/// 1. **Version check** — `version` must be non-empty and parse as semver `x.y.z`.
425/// 2. **Business-rule constraints** — `appCode` length, hook address format, `partnerFee`
426///    basis-point caps, and similar field-level rules enforced by the private `validation` helper
427///    module.
428/// 3. **JSON Schema validation** — *(only when the `schema-validation` feature is enabled; on by
429///    default for native targets, off by default on wasm)* the serialised document is checked
430///    against the bundled upstream schema via the `schema` module, catching structural drift that
431///    the hand-written business rules do not cover (missing required fields, unknown properties,
432///    regex violations, `anyOf` variants, …).
433///
434/// Returns a [`ValidationResult`] that lists every violation found. An
435/// empty [`ValidationResult::typed_errors`] list means the document is
436/// fully valid.
437///
438/// # Example
439///
440/// ```
441/// use cow_app_data::{AppDataDoc, validate_app_data_doc};
442///
443/// let doc = AppDataDoc::new("CoW Swap");
444/// let result = validate_app_data_doc(&doc);
445/// assert!(result.is_valid());
446/// assert!(!result.has_errors());
447/// ```
448#[must_use]
449pub fn validate_app_data_doc(doc: &AppDataDoc) -> ValidationResult {
450    let mut typed_errors: Vec<ValidationError> = Vec::new();
451
452    // ── Version check ──────────────────────────────────────────────────────
453    if doc.version.is_empty() {
454        typed_errors.push(ValidationError::InvalidVersion("version must not be empty".to_owned()));
455    } else {
456        // Expect semver format: \d+\.\d+\.\d+
457        let parts: Vec<&str> = doc.version.split('.').collect();
458        if parts.len() != 3 || parts.iter().any(|p| p.parse::<u32>().is_err()) {
459            typed_errors.push(ValidationError::InvalidVersion(format!(
460                "version '{}' is not valid semver",
461                doc.version
462            )));
463        }
464    }
465
466    // ── Business-rule checks (appCode, hooks, partnerFee, orderClass, …) ──
467    validate_constraints(doc, &mut typed_errors);
468
469    // ── Structural JSON Schema check ───────────────────────────────────────
470    //
471    // Dispatches on `doc.version`: [`super::schema::validate`] selects the
472    // bundled schema matching the document's declared version and returns
473    // either a list of violations or an `UnsupportedVersion` error. Both
474    // outcomes flow into the combined `ValidationResult`.
475    #[cfg(feature = "schema-validation")]
476    match super::schema::validate(doc) {
477        Ok(()) => {}
478        Err(super::schema::SchemaError::Violations(violations)) => {
479            for v in violations {
480                typed_errors
481                    .push(ValidationError::SchemaViolation { path: v.path, message: v.message });
482            }
483        }
484        Err(super::schema::SchemaError::UnsupportedVersion { requested, supported }) => {
485            typed_errors.push(ValidationError::SchemaViolation {
486                path: "/version".to_owned(),
487                message: format!(
488                    "AppData version `{requested}` is not backed by a bundled schema in \
489                     this build (supported: {})",
490                    supported.join(", ")
491                ),
492            });
493        }
494    }
495
496    // Render string representations once, in sync with the typed list.
497    let string_errors: Vec<String> = typed_errors.iter().map(|e| e.to_string()).collect();
498
499    let success = typed_errors.is_empty();
500    ValidationResult { success, errors: string_errors, typed_errors }
501}
502
503// ── IPFS fetch ────────────────────────────────────────────────────────────────
504
505/// Fetch an [`AppDataDoc`] from IPFS by its `CIDv1`.
506///
507/// Sends a GET request to `{ipfs_uri}/{cid}` and deserialises the JSON
508/// response into an [`AppDataDoc`].
509///
510/// # Parameters
511///
512/// * `cid` — the `CIDv1` base16 string identifying the document.
513/// * `ipfs_uri` — optional gateway base URL. Defaults to [`DEFAULT_IPFS_READ_URI`] when `None`.
514///
515/// # Returns
516///
517/// The deserialised [`AppDataDoc`].
518///
519/// # Errors
520///
521/// Returns [`CowError::Http`] or [`CowError::Parse`] on failure.
522///
523/// # Example
524///
525/// ```no_run
526/// use cow_app_data::fetch_doc_from_cid;
527///
528/// # async fn example() -> Result<(), cow_errors::CowError> {
529/// let cid = "bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi";
530/// let doc = fetch_doc_from_cid(cid, None).await?;
531/// assert!(!doc.version.is_empty());
532/// # Ok(())
533/// # }
534/// ```
535pub async fn fetch_doc_from_cid(cid: &str, ipfs_uri: Option<&str>) -> Result<AppDataDoc, CowError> {
536    let base = ipfs_uri.map_or(DEFAULT_IPFS_READ_URI, |s| s);
537    let url = format!("{base}/{cid}");
538    let text = reqwest::get(&url).await?.text().await?;
539    serde_json::from_str(&text)
540        .map_err(|e| CowError::Parse { field: "app_data_doc", reason: e.to_string() })
541}
542
543/// Fetch an [`AppDataDoc`] from IPFS using a hex `appData` value.
544///
545/// Converts `app_data_hex` to a `CIDv1` via
546/// [`appdata_hex_to_cid`], then delegates
547/// to [`fetch_doc_from_cid`] for the actual HTTP fetch.
548///
549/// # Parameters
550///
551/// * `app_data_hex` — the `0x`-prefixed 32-byte hex value from the on-chain order struct.
552/// * `ipfs_uri` — optional gateway base URL (defaults to [`DEFAULT_IPFS_READ_URI`]).
553///
554/// # Returns
555///
556/// The deserialised [`AppDataDoc`].
557///
558/// # Errors
559///
560/// Returns [`CowError::AppData`], [`CowError::Http`], or [`CowError::Parse`].
561///
562/// # Example
563///
564/// ```no_run
565/// use cow_app_data::fetch_doc_from_app_data_hex;
566///
567/// # async fn example() -> Result<(), cow_errors::CowError> {
568/// let hex = "0x0000000000000000000000000000000000000000000000000000000000000000";
569/// let doc = fetch_doc_from_app_data_hex(hex, None).await?;
570/// assert!(!doc.version.is_empty());
571/// # Ok(())
572/// # }
573/// ```
574pub async fn fetch_doc_from_app_data_hex(
575    app_data_hex: &str,
576    ipfs_uri: Option<&str>,
577) -> Result<AppDataDoc, CowError> {
578    let cid = appdata_hex_to_cid(app_data_hex)?;
579    fetch_doc_from_cid(&cid, ipfs_uri).await
580}
581
582// ── IPFS upload ───────────────────────────────────────────────────────────────
583
584/// Response from the Pinata `pinJSONToIPFS` endpoint.
585#[derive(Debug, Deserialize)]
586#[serde(rename_all = "PascalCase")]
587struct PinataResponse {
588    ipfs_hash: String,
589}
590
591/// Upload an [`AppDataDoc`] to IPFS via the Pinata pinning service.
592///
593/// The document is first serialised to deterministic JSON and hashed via
594/// [`get_app_data_info`], then uploaded to
595/// `{write_uri}/pinning/pinJSONToIPFS` using the provided Pinata API
596/// credentials. The canonical JSON is pinned as `pinataContent` and the
597/// `keccak256` hex is stored as `pinataMetadata.name`.
598///
599/// # Parameters
600///
601/// * `doc` — the [`AppDataDoc`] to upload.
602/// * `ipfs` — the [`Ipfs`] configuration containing Pinata credentials and optional gateway URIs.
603///
604/// # Returns
605///
606/// The IPFS CID hash string returned by Pinata on success.
607///
608/// # Errors
609///
610/// Returns [`CowError::AppData`] when no Pinata credentials are configured,
611/// [`CowError::Http`] on transport failure, or [`CowError::Api`] when Pinata
612/// returns a non-2xx status code.
613///
614/// # Example
615///
616/// ```no_run
617/// use cow_app_data::{AppDataDoc, Ipfs, upload_app_data_to_pinata};
618///
619/// # async fn example() -> Result<(), cow_errors::CowError> {
620/// let doc = AppDataDoc::new("CoW Swap");
621/// let ipfs = Ipfs::default().with_pinata("my-api-key", "my-api-secret");
622/// let cid = upload_app_data_to_pinata(&doc, &ipfs).await?;
623/// assert!(!cid.is_empty());
624/// # Ok(())
625/// # }
626/// ```
627pub async fn upload_app_data_to_pinata(doc: &AppDataDoc, ipfs: &Ipfs) -> Result<String, CowError> {
628    let api_key = ipfs
629        .pinata_api_key
630        .as_deref()
631        .ok_or_else(|| CowError::AppData("pinata_api_key is required for IPFS upload".into()))?;
632    let api_secret = ipfs
633        .pinata_api_secret
634        .as_deref()
635        .ok_or_else(|| CowError::AppData("pinata_api_secret is required for IPFS upload".into()))?;
636
637    let info = get_app_data_info(doc)?;
638    let write_uri = ipfs.write_uri.as_deref().map_or(DEFAULT_IPFS_WRITE_URI, |s| s);
639    let url = format!("{write_uri}/pinning/pinJSONToIPFS");
640
641    let content: serde_json::Value = serde_json::from_str(&info.app_data_content)
642        .map_err(|e| CowError::AppData(e.to_string()))?;
643
644    let body = json!({
645        "pinataContent": content,
646        "pinataOptions": { "cidVersion": 1 },
647        "pinataMetadata": { "name": info.app_data_hex }
648    });
649
650    let resp = reqwest::Client::new()
651        .post(&url)
652        .header("pinata_api_key", api_key)
653        .header("pinata_secret_api_key", api_secret)
654        .json(&body)
655        .send()
656        .await?;
657
658    let status = resp.status().as_u16();
659    let text = resp.text().await?;
660    if status != 200 {
661        return Err(CowError::Api { status, body: text });
662    }
663
664    let pinata: PinataResponse =
665        serde_json::from_str(&text).map_err(|e| CowError::AppData(e.to_string()))?;
666    Ok(pinata.ipfs_hash)
667}
668
669// ── Legacy helpers ───────────────────────────────────────────────────────────
670
671/// Internal helper for deriving [`AppDataInfo`] from either a document or a
672/// pre-serialised JSON string, using a pluggable CID derivation function.
673///
674/// This is the Rust equivalent of `_appDataToCidAux` in the `TypeScript` SDK.
675#[allow(
676    clippy::type_complexity,
677    reason = "mirrors the TypeScript SDK's pluggable CID derivation pattern"
678)]
679fn app_data_to_cid_aux(
680    full_app_data: &str,
681    derive_cid: fn(&str) -> Result<String, CowError>,
682) -> Result<AppDataInfo, CowError> {
683    let cid = derive_cid(full_app_data)?;
684    let app_data_hex = extract_digest(&cid)?;
685
686    if app_data_hex.is_empty() {
687        return Err(CowError::AppData(format!(
688            "Could not extract appDataHex from calculated cid {cid}"
689        )));
690    }
691
692    Ok(AppDataInfo { cid, app_data_content: full_app_data.to_owned(), app_data_hex })
693}
694
695/// Internal CID derivation using the legacy `sha2-256` / `dag-pb` method.
696///
697/// **Note**: The original `TypeScript` SDK used `ipfs-only-hash` with `CIDv0`. This Rust
698/// implementation uses `keccak256` as the hash but wraps it in the legacy CID
699/// prefix for structural compatibility. True legacy CID reproduction would
700/// require an `sha2-256` IPFS chunker which is not included.
701///
702/// This is the Rust equivalent of `_appDataToCidLegacy` in the `TypeScript` SDK.
703#[allow(deprecated, reason = "wraps the deprecated legacy CID function intentionally")]
704fn app_data_to_cid_legacy(full_app_data_json: &str) -> Result<String, CowError> {
705    let hash = alloy_primitives::keccak256(full_app_data_json.as_bytes());
706    let app_data_hex = format!("0x{}", alloy_primitives::hex::encode(hash.as_slice()));
707    super::cid::app_data_hex_to_cid_legacy(&app_data_hex)
708}
709
710/// Derive [`AppDataInfo`] using the legacy method.
711///
712/// Uses `JSON.stringify`-equivalent serialisation (plain `serde_json::to_string`) and
713/// legacy CID encoding for backwards compatibility.
714///
715/// # Errors
716///
717/// Returns [`CowError::AppData`] on serialisation or CID failure.
718#[deprecated(
719    note = "Use get_app_data_info instead — legacy CID encoding is no longer used by CoW Protocol"
720)]
721pub fn get_app_data_info_legacy(doc: &AppDataDoc) -> Result<AppDataInfo, CowError> {
722    // Legacy mode uses plain JSON.stringify (non-deterministic key order)
723    let full_app_data = serde_json::to_string(doc).map_err(|e| CowError::AppData(e.to_string()))?;
724    app_data_to_cid_aux(&full_app_data, app_data_to_cid_legacy)
725}
726
727/// Internal helper that fetches a document from IPFS via a pluggable
728/// hex-to-CID conversion function.
729///
730/// This is the Rust equivalent of `_fetchDocFromCidAux` in the `TypeScript` SDK.
731#[allow(
732    clippy::type_complexity,
733    reason = "mirrors the TypeScript SDK's pluggable hex-to-CID conversion pattern"
734)]
735async fn fetch_doc_from_cid_aux(
736    hex_to_cid: fn(&str) -> Result<String, CowError>,
737    app_data_hex: &str,
738    ipfs_uri: Option<&str>,
739) -> Result<AppDataDoc, CowError> {
740    let cid = hex_to_cid(app_data_hex).map_err(|e| {
741        CowError::AppData(format!("Error decoding AppData: appDataHex={app_data_hex}, message={e}"))
742    })?;
743
744    if cid.is_empty() {
745        return Err(CowError::AppData("Error getting serialized CID".into()));
746    }
747
748    fetch_doc_from_cid(&cid, ipfs_uri).await
749}
750
751/// Fetch an [`AppDataDoc`] from IPFS using the legacy CID derivation method.
752///
753/// Converts `app_data_hex` to a CID using the legacy `dag-pb` / `sha2-256`
754/// encoding, then fetches the content from IPFS.
755///
756/// # Errors
757///
758/// Returns [`CowError::AppData`], [`CowError::Http`], or [`CowError::Parse`].
759#[deprecated(
760    note = "Use fetch_doc_from_app_data_hex instead — legacy CID encoding is no longer used by CoW Protocol"
761)]
762#[allow(
763    deprecated,
764    reason = "this function is itself deprecated and wraps other deprecated functions"
765)]
766pub async fn fetch_doc_from_app_data_hex_legacy(
767    app_data_hex: &str,
768    ipfs_uri: Option<&str>,
769) -> Result<AppDataDoc, CowError> {
770    fetch_doc_from_cid_aux(super::cid::app_data_hex_to_cid_legacy, app_data_hex, ipfs_uri).await
771}
772
773/// Upload an [`AppDataDoc`] to IPFS via Pinata using the legacy method.
774///
775/// The document is pinned to Pinata, and the resulting CID is used to extract
776/// the `appData` hex digest.
777///
778/// # Errors
779///
780/// Returns [`CowError::AppData`] when credentials are missing or the CID
781/// extraction fails, [`CowError::Http`] on transport failure, or
782/// [`CowError::Api`] on a non-2xx Pinata response.
783#[deprecated(
784    note = "Use upload_app_data_to_pinata instead — legacy Pinata pinning relied on implicit encoding"
785)]
786pub async fn upload_metadata_doc_to_ipfs_legacy(
787    doc: &AppDataDoc,
788    ipfs: &Ipfs,
789) -> Result<IpfsUploadResult, CowError> {
790    let cid = upload_app_data_to_pinata_legacy(doc, ipfs).await?;
791    let app_data = extract_digest(&cid)?;
792    Ok(IpfsUploadResult { app_data, cid })
793}
794
795/// Result of uploading metadata to IPFS (legacy).
796#[derive(Debug, Clone)]
797pub struct IpfsUploadResult {
798    /// The `appData` hex digest extracted from the CID.
799    pub app_data: String,
800    /// The IPFS CID of the pinned content.
801    pub cid: String,
802}
803
804impl fmt::Display for IpfsUploadResult {
805    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
806        write!(f, "ipfs-upload(cid={}, appData={})", self.cid, self.app_data)
807    }
808}
809
810/// Internal legacy Pinata upload (pins with default `CIDv0` encoding).
811///
812/// This is the Rust equivalent of `_pinJsonInPinataIpfs` in the `TypeScript` SDK.
813async fn upload_app_data_to_pinata_legacy(
814    doc: &AppDataDoc,
815    ipfs: &Ipfs,
816) -> Result<String, CowError> {
817    let api_key = ipfs
818        .pinata_api_key
819        .as_deref()
820        .ok_or_else(|| CowError::AppData("You need to pass IPFS api credentials.".into()))?;
821    let api_secret = ipfs
822        .pinata_api_secret
823        .as_deref()
824        .ok_or_else(|| CowError::AppData("You need to pass IPFS api credentials.".into()))?;
825
826    if api_key.is_empty() || api_secret.is_empty() {
827        return Err(CowError::AppData("You need to pass IPFS api credentials.".into()));
828    }
829
830    let content: serde_json::Value =
831        serde_json::to_value(doc).map_err(|e| CowError::AppData(e.to_string()))?;
832
833    let body = json!({
834        "pinataContent": content,
835        "pinataMetadata": { "name": "appData" }
836    });
837
838    let write_uri = ipfs.write_uri.as_deref().map_or(DEFAULT_IPFS_WRITE_URI, |s| s);
839    let url = format!("{write_uri}/pinning/pinJSONToIPFS");
840
841    let resp = reqwest::Client::new()
842        .post(&url)
843        .header("Content-Type", "application/json")
844        .header("pinata_api_key", api_key)
845        .header("pinata_secret_api_key", api_secret)
846        .json(&body)
847        .send()
848        .await?;
849
850    let status = resp.status().as_u16();
851    let text = resp.text().await?;
852    if status != 200 {
853        return Err(CowError::Api { status, body: text });
854    }
855
856    let pinata: PinataResponse =
857        serde_json::from_str(&text).map_err(|e| CowError::AppData(e.to_string()))?;
858    Ok(pinata.ipfs_hash)
859}
860
861// ── Schema helpers ──────────────────────────────────────────────────────────
862
863/// Known app-data schema versions.
864const KNOWN_SCHEMA_VERSIONS: &[&str] = &["0.7.0", "1.3.0"];
865
866/// Import (look up) an app-data schema by version string.
867///
868/// In the `TypeScript` SDK this dynamically imports a JSON schema file. In
869/// Rust, supported schema versions are compiled-in. Returns a placeholder
870/// [`AppDataDoc`] with the `version` field set to indicate which schema was
871/// requested.
872///
873/// Currently known versions: `"0.7.0"`, `"1.3.0"`.
874///
875/// Mirrors `importSchema` from the `@cowprotocol/app-data` `TypeScript`
876/// package.
877///
878/// # Parameters
879///
880/// * `version` — semver string (e.g. `"1.3.0"`).
881///
882/// # Returns
883///
884/// An [`AppDataDoc`] with the requested version and empty metadata.
885///
886/// # Errors
887///
888/// Returns [`CowError::AppData`] if `version` is not a valid semver string
889/// or is not a known schema version.
890///
891/// # Example
892///
893/// ```
894/// use cow_app_data::import_schema;
895///
896/// let doc = import_schema("1.3.0").unwrap();
897/// assert_eq!(doc.version, "1.3.0");
898///
899/// assert!(import_schema("99.0.0").is_err()); // unknown version
900/// assert!(import_schema("not-semver").is_err());
901/// ```
902pub fn import_schema(version: &str) -> Result<AppDataDoc, CowError> {
903    // Validate semver format
904    let re_parts: Vec<&str> = version.split('.').collect();
905    if re_parts.len() != 3 || re_parts.iter().any(|p| p.parse::<u32>().is_err()) {
906        return Err(CowError::AppData(format!("AppData version {version} is not a valid version")));
907    }
908
909    if !KNOWN_SCHEMA_VERSIONS.contains(&version) {
910        return Err(CowError::AppData(format!("AppData version {version} doesn't exist")));
911    }
912
913    Ok(AppDataDoc {
914        version: version.to_owned(),
915        app_code: None,
916        environment: None,
917        metadata: Metadata::default(),
918    })
919}
920
921/// Get the app-data schema for a given version.
922///
923/// Wraps [`import_schema`] and converts errors to [`CowError::AppData`].
924///
925/// Mirrors `getAppDataSchema` from the `@cowprotocol/app-data` `TypeScript`
926/// package.
927///
928/// # Parameters
929///
930/// * `version` — semver string (e.g. `"1.3.0"`).
931///
932/// # Returns
933///
934/// An [`AppDataDoc`] placeholder with the requested version.
935///
936/// # Errors
937///
938/// Returns [`CowError::AppData`] when the version doesn't exist or is not
939/// valid semver.
940pub fn get_app_data_schema(version: &str) -> Result<AppDataDoc, CowError> {
941    import_schema(version).map_err(|e| CowError::AppData(format!("{e}")))
942}
943
944// ── MetadataApi ───────────────────────────────────────────────────────────────
945
946/// High-level facade mirroring `MetadataApi` from the `TypeScript` SDK.
947///
948/// All operations are available as free functions in this module;
949/// `MetadataApi` groups them under a single type that carries an optional
950/// [`Ipfs`] configuration, so callers do not have to thread IPFS settings
951/// through every call.
952///
953/// # Typical workflow
954///
955/// ```rust
956/// use cow_app_data::{Ipfs, MetadataApi};
957///
958/// // 1. Create the API (with optional IPFS config).
959/// let api = MetadataApi::new();
960///
961/// // 2. Build an app-data document.
962/// let doc = api.generate_app_data_doc("MyDApp");
963///
964/// // 3. Validate it.
965/// let result = api.validate_app_data_doc(&doc);
966/// assert!(result.is_valid());
967///
968/// // 4. Derive the hash + CID.
969/// let info = api.get_app_data_info(&doc).unwrap();
970/// assert!(info.app_data_hex.starts_with("0x"));
971/// ```
972#[derive(Debug, Clone, Default)]
973pub struct MetadataApi {
974    /// Optional IPFS configuration.
975    pub ipfs: Ipfs,
976}
977
978impl MetadataApi {
979    /// Create a new [`MetadataApi`] with default IPFS settings.
980    ///
981    /// Uses [`DEFAULT_IPFS_READ_URI`] for fetching and
982    /// [`DEFAULT_IPFS_WRITE_URI`] for uploads. No Pinata credentials are
983    /// configured — set them via [`with_ipfs`](Self::with_ipfs) if you need
984    /// upload capability.
985    ///
986    /// # Returns
987    ///
988    /// A new [`MetadataApi`] with default [`Ipfs`] configuration.
989    #[must_use]
990    pub fn new() -> Self {
991        Self::default()
992    }
993
994    /// Create a [`MetadataApi`] with custom IPFS configuration.
995    ///
996    /// # Parameters
997    ///
998    /// * `ipfs` — the [`Ipfs`] settings (gateway URIs, Pinata credentials).
999    ///
1000    /// # Returns
1001    ///
1002    /// A new [`MetadataApi`] using the given configuration.
1003    ///
1004    /// # Example
1005    ///
1006    /// ```
1007    /// use cow_app_data::{Ipfs, MetadataApi};
1008    ///
1009    /// let api = MetadataApi::with_ipfs(
1010    ///     Ipfs::default().with_read_uri("https://my-gateway.io/ipfs").with_pinata("key", "secret"),
1011    /// );
1012    /// ```
1013    #[must_use]
1014    pub const fn with_ipfs(ipfs: Ipfs) -> Self {
1015        Self { ipfs }
1016    }
1017
1018    /// Generate a minimal [`AppDataDoc`] for `app_code`.
1019    ///
1020    /// # Example
1021    ///
1022    /// ```
1023    /// use cow_app_data::MetadataApi;
1024    ///
1025    /// let api = MetadataApi::new();
1026    /// let doc = api.generate_app_data_doc("CoW Swap");
1027    /// assert_eq!(doc.app_code.as_deref(), Some("CoW Swap"));
1028    /// ```
1029    #[must_use]
1030    pub fn generate_app_data_doc(&self, app_code: impl Into<String>) -> AppDataDoc {
1031        AppDataDoc::new(app_code)
1032    }
1033
1034    /// Validate an [`AppDataDoc`].
1035    ///
1036    /// # Example
1037    ///
1038    /// ```
1039    /// use cow_app_data::{AppDataDoc, MetadataApi};
1040    ///
1041    /// let api = MetadataApi::new();
1042    /// let doc = AppDataDoc::new("CoW Swap");
1043    /// let result = api.validate_app_data_doc(&doc);
1044    /// assert!(result.is_valid());
1045    /// ```
1046    #[must_use]
1047    pub fn validate_app_data_doc(&self, doc: &AppDataDoc) -> ValidationResult {
1048        validate_app_data_doc(doc)
1049    }
1050
1051    /// Compute the `keccak256` hash of `doc` as a [`B256`].
1052    ///
1053    /// Delegates to [`appdata_hex`]. The document
1054    /// is serialised to deterministic JSON before hashing.
1055    ///
1056    /// # Parameters
1057    ///
1058    /// * `doc` — the [`AppDataDoc`] to hash.
1059    ///
1060    /// # Returns
1061    ///
1062    /// A 32-byte [`B256`] digest.
1063    ///
1064    /// # Errors
1065    ///
1066    /// Propagates [`CowError::AppData`] on serialisation failure.
1067    pub fn appdata_hex(&self, doc: &AppDataDoc) -> Result<B256, CowError> {
1068        appdata_hex(doc)
1069    }
1070
1071    /// Derive the full [`AppDataInfo`] (JSON content, hex hash, CID) from
1072    /// `doc`.
1073    ///
1074    /// Delegates to [`get_app_data_info`]. This is the most common method
1075    /// for obtaining everything needed to submit an order and pin data on
1076    /// IPFS.
1077    ///
1078    /// # Parameters
1079    ///
1080    /// * `doc` — the [`AppDataDoc`] to process.
1081    ///
1082    /// # Returns
1083    ///
1084    /// An [`AppDataInfo`] with canonical JSON, hex hash, and CID.
1085    ///
1086    /// # Errors
1087    ///
1088    /// Propagates [`CowError::AppData`].
1089    ///
1090    /// # Example
1091    ///
1092    /// ```
1093    /// use cow_app_data::{AppDataDoc, MetadataApi};
1094    ///
1095    /// let api = MetadataApi::new();
1096    /// let doc = AppDataDoc::new("CoW Swap");
1097    /// let info = api.get_app_data_info(&doc)?;
1098    /// assert!(info.app_data_hex.starts_with("0x"));
1099    /// # Ok::<(), cow_errors::CowError>(())
1100    /// ```
1101    pub fn get_app_data_info(&self, doc: &AppDataDoc) -> Result<AppDataInfo, CowError> {
1102        get_app_data_info(doc)
1103    }
1104
1105    /// Derive [`AppDataInfo`] from a pre-serialised JSON string.
1106    ///
1107    /// Hashes `json` directly without re-serialising, preserving the exact
1108    /// byte sequence as the canonical `keccak256` pre-image.
1109    ///
1110    /// # Parameters
1111    ///
1112    /// * `json` — the canonical JSON string.
1113    ///
1114    /// # Returns
1115    ///
1116    /// An [`AppDataInfo`] where `app_data_content` is `json` verbatim.
1117    ///
1118    /// # Errors
1119    ///
1120    /// Propagates [`CowError::AppData`].
1121    pub fn get_app_data_info_from_str(&self, json: &str) -> Result<AppDataInfo, CowError> {
1122        get_app_data_info_from_str(json)
1123    }
1124
1125    /// Convert `app_data_hex` to a `CIDv1` base16 string.
1126    ///
1127    /// Delegates to
1128    /// [`appdata_hex_to_cid`].
1129    ///
1130    /// # Parameters
1131    ///
1132    /// * `app_data_hex` — the `appData` hex value, with or without `0x`.
1133    ///
1134    /// # Returns
1135    ///
1136    /// A base16 `CIDv1` string (prefix `f`).
1137    ///
1138    /// # Errors
1139    ///
1140    /// Propagates [`CowError::AppData`].
1141    pub fn app_data_hex_to_cid(&self, app_data_hex: &str) -> Result<String, CowError> {
1142        appdata_hex_to_cid(app_data_hex)
1143    }
1144
1145    /// Extract the `appData` hex digest from a `CIDv1` string.
1146    ///
1147    /// Delegates to
1148    /// [`cid_to_appdata_hex`].
1149    ///
1150    /// # Parameters
1151    ///
1152    /// * `cid` — a base16 multibase CID string.
1153    ///
1154    /// # Returns
1155    ///
1156    /// A `0x`-prefixed hex string of the 32-byte digest.
1157    ///
1158    /// # Errors
1159    ///
1160    /// Propagates [`CowError::AppData`].
1161    pub fn cid_to_app_data_hex(&self, cid: &str) -> Result<String, CowError> {
1162        cid_to_appdata_hex(cid)
1163    }
1164
1165    /// Fetch an [`AppDataDoc`] from IPFS by `CIDv1`.
1166    ///
1167    /// Uses the configured `ipfs.read_uri` or [`DEFAULT_IPFS_READ_URI`]
1168    /// when no custom gateway is set.
1169    ///
1170    /// # Parameters
1171    ///
1172    /// * `cid` — the `CIDv1` base16 string identifying the document on IPFS.
1173    ///
1174    /// # Returns
1175    ///
1176    /// The deserialised [`AppDataDoc`].
1177    ///
1178    /// # Errors
1179    ///
1180    /// Propagates [`CowError::Http`] on network failure or
1181    /// [`CowError::Parse`] if the response is not valid JSON.
1182    pub async fn fetch_doc_from_cid(&self, cid: &str) -> Result<AppDataDoc, CowError> {
1183        let uri = self.ipfs.read_uri.as_deref();
1184        fetch_doc_from_cid(cid, uri).await
1185    }
1186
1187    /// Fetch an [`AppDataDoc`] from IPFS by `appData` hex value.
1188    ///
1189    /// Converts `app_data_hex` to a `CIDv1`, then fetches the document
1190    /// from the configured IPFS gateway.
1191    ///
1192    /// # Parameters
1193    ///
1194    /// * `app_data_hex` — the `0x`-prefixed 32-byte hex value.
1195    ///
1196    /// # Returns
1197    ///
1198    /// The deserialised [`AppDataDoc`].
1199    ///
1200    /// # Errors
1201    ///
1202    /// Propagates [`CowError::AppData`], [`CowError::Http`], or
1203    /// [`CowError::Parse`].
1204    pub async fn fetch_doc_from_app_data_hex(
1205        &self,
1206        app_data_hex: &str,
1207    ) -> Result<AppDataDoc, CowError> {
1208        let uri = self.ipfs.read_uri.as_deref();
1209        fetch_doc_from_app_data_hex(app_data_hex, uri).await
1210    }
1211
1212    /// Upload `doc` to IPFS via the Pinata pinning service.
1213    ///
1214    /// The document is serialised to deterministic JSON, hashed, and
1215    /// pinned to Pinata. Requires [`Ipfs::pinata_api_key`] and
1216    /// [`Ipfs::pinata_api_secret`] to be set on the configured [`Ipfs`]
1217    /// instance.
1218    ///
1219    /// # Parameters
1220    ///
1221    /// * `doc` — the [`AppDataDoc`] to upload.
1222    ///
1223    /// # Returns
1224    ///
1225    /// The IPFS `CIDv1` hash string of the pinned content.
1226    ///
1227    /// # Errors
1228    ///
1229    /// Returns [`CowError::AppData`] when credentials are missing,
1230    /// [`CowError::Http`] on transport failure, or [`CowError::Api`] on a
1231    /// non-2xx Pinata response.
1232    pub async fn upload_app_data(&self, doc: &AppDataDoc) -> Result<String, CowError> {
1233        upload_app_data_to_pinata(doc, &self.ipfs).await
1234    }
1235
1236    /// Derive [`AppDataInfo`] using the legacy CID encoding method.
1237    ///
1238    /// # Errors
1239    ///
1240    /// Propagates [`CowError::AppData`].
1241    #[deprecated(note = "Use get_app_data_info instead")]
1242    #[allow(
1243        deprecated,
1244        reason = "this method is itself deprecated and delegates to a deprecated function"
1245    )]
1246    pub fn get_app_data_info_legacy(&self, doc: &AppDataDoc) -> Result<AppDataInfo, CowError> {
1247        get_app_data_info_legacy(doc)
1248    }
1249
1250    /// Fetch an [`AppDataDoc`] from IPFS using the legacy CID derivation.
1251    ///
1252    /// # Errors
1253    ///
1254    /// Propagates [`CowError::AppData`], [`CowError::Http`], or [`CowError::Parse`].
1255    #[deprecated(note = "Use fetch_doc_from_app_data_hex instead")]
1256    #[allow(
1257        deprecated,
1258        reason = "this method is itself deprecated and delegates to a deprecated function"
1259    )]
1260    pub async fn fetch_doc_from_app_data_hex_legacy(
1261        &self,
1262        app_data_hex: &str,
1263    ) -> Result<AppDataDoc, CowError> {
1264        let uri = self.ipfs.read_uri.as_deref();
1265        fetch_doc_from_app_data_hex_legacy(app_data_hex, uri).await
1266    }
1267
1268    /// Upload `doc` to IPFS via Pinata using the legacy method.
1269    ///
1270    /// # Errors
1271    ///
1272    /// Returns [`CowError::AppData`], [`CowError::Http`], or [`CowError::Api`].
1273    #[deprecated(note = "Use upload_app_data instead")]
1274    #[allow(
1275        deprecated,
1276        reason = "this method is itself deprecated and delegates to a deprecated function"
1277    )]
1278    pub async fn upload_metadata_doc_to_ipfs_legacy(
1279        &self,
1280        doc: &AppDataDoc,
1281    ) -> Result<IpfsUploadResult, CowError> {
1282        upload_metadata_doc_to_ipfs_legacy(doc, &self.ipfs).await
1283    }
1284
1285    /// Get the app-data schema for a given version.
1286    ///
1287    /// Delegates to [`get_app_data_schema`]. Currently known versions:
1288    /// `"0.7.0"`, `"1.3.0"`.
1289    ///
1290    /// # Parameters
1291    ///
1292    /// * `version` — semver string (e.g. `"1.3.0"`).
1293    ///
1294    /// # Returns
1295    ///
1296    /// An [`AppDataDoc`] placeholder with the requested version.
1297    ///
1298    /// # Errors
1299    ///
1300    /// Returns [`CowError::AppData`] when the version doesn't exist.
1301    pub fn get_app_data_schema(&self, version: &str) -> Result<AppDataDoc, CowError> {
1302        get_app_data_schema(version)
1303    }
1304
1305    /// Import a schema by version string.
1306    ///
1307    /// Delegates to [`import_schema`].
1308    ///
1309    /// # Parameters
1310    ///
1311    /// * `version` — semver string.
1312    ///
1313    /// # Returns
1314    ///
1315    /// An [`AppDataDoc`] placeholder with the requested version.
1316    ///
1317    /// # Errors
1318    ///
1319    /// Returns [`CowError::AppData`] if the version is invalid or unknown.
1320    pub fn import_schema(&self, version: &str) -> Result<AppDataDoc, CowError> {
1321        import_schema(version)
1322    }
1323
1324    /// Convert `app_data_hex` to a CID using the legacy method.
1325    ///
1326    /// # Errors
1327    ///
1328    /// Propagates [`CowError::AppData`].
1329    #[deprecated(note = "Use app_data_hex_to_cid instead")]
1330    #[allow(
1331        deprecated,
1332        reason = "this method is itself deprecated and delegates to a deprecated function"
1333    )]
1334    pub fn app_data_hex_to_cid_legacy(&self, app_data_hex: &str) -> Result<String, CowError> {
1335        super::cid::app_data_hex_to_cid_legacy(app_data_hex)
1336    }
1337
1338    /// Parse a CID string into its constituent [`CidComponents`](super::cid::CidComponents).
1339    ///
1340    /// Delegates to [`parse_cid`](super::cid::parse_cid). Only base16
1341    /// CIDs (prefix `f` or `F`) are supported.
1342    ///
1343    /// # Parameters
1344    ///
1345    /// * `ipfs_hash` — a multibase-encoded CID string.
1346    ///
1347    /// # Returns
1348    ///
1349    /// A [`CidComponents`](super::cid::CidComponents) with version, codec,
1350    /// hash function, hash length, and raw digest.
1351    ///
1352    /// # Errors
1353    ///
1354    /// Propagates [`CowError::AppData`].
1355    pub fn parse_cid(&self, ipfs_hash: &str) -> Result<super::cid::CidComponents, CowError> {
1356        super::cid::parse_cid(ipfs_hash)
1357    }
1358
1359    /// Decode raw CID bytes into their constituent [`CidComponents`](super::cid::CidComponents).
1360    ///
1361    /// Delegates to [`decode_cid`](super::cid::decode_cid).
1362    ///
1363    /// # Parameters
1364    ///
1365    /// * `bytes` — raw CID bytes (`[version, codec, hash_fn, hash_len, ...digest]`).
1366    ///
1367    /// # Returns
1368    ///
1369    /// A [`CidComponents`](super::cid::CidComponents) with the parsed fields.
1370    ///
1371    /// # Errors
1372    ///
1373    /// Propagates [`CowError::AppData`] if the slice is too short.
1374    pub fn decode_cid(&self, bytes: &[u8]) -> Result<super::cid::CidComponents, CowError> {
1375        super::cid::decode_cid(bytes)
1376    }
1377
1378    /// Extract the multihash digest from a CID string as `0x`-prefixed hex.
1379    ///
1380    /// Delegates to [`extract_digest`].
1381    ///
1382    /// # Parameters
1383    ///
1384    /// * `cid` — a base16 multibase CID string.
1385    ///
1386    /// # Returns
1387    ///
1388    /// A `0x`-prefixed hex string of the raw digest bytes.
1389    ///
1390    /// # Errors
1391    ///
1392    /// Propagates [`CowError::AppData`].
1393    pub fn extract_digest(&self, cid: &str) -> Result<String, CowError> {
1394        super::cid::extract_digest(cid)
1395    }
1396}
1397
1398impl fmt::Display for MetadataApi {
1399    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1400        f.write_str("metadata-api")
1401    }
1402}