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}