Skip to main content

dig_offers/
hydrate.rs

1//! Decode an `offer1…` string, and parse it into a spendable [`Offer`] within a caller-owned
2//! [`SpendContext`].
3//!
4//! These two primitives enforce **one-context locality**: a parsed [`Offer`] holds
5//! allocator-relative pointers (an offered NFT's metadata is a `HashedPtr`), so any build that
6//! reconstructs those assets — taking, cancelling, combining — MUST parse and build in the SAME
7//! context. Callers that need both steps use [`decode`] then [`parse`] against one `ctx`.
8
9use chia_protocol::SpendBundle;
10use chia_wallet_sdk::driver::{decode_offer, Offer, SpendContext};
11
12use crate::error::{Error, Result};
13
14/// Decode a bech32 `offer1…` string into the maker's [`SpendBundle`].
15///
16/// Rejects, with a clear message, anything that is not a valid current-format Chia offer — so a
17/// caller surfaces an honest "this isn't an offer" rather than a cryptic failure deep in a build.
18/// This is a pure codec step: it allocates nothing in a build context and performs no I/O.
19pub fn decode(offer_str: &str) -> Result<SpendBundle> {
20    let trimmed = offer_str.trim();
21    if !trimmed.starts_with("offer1") {
22        return Err(Error::decode(
23            "not a Chia offer: expected a bech32 string starting with 'offer1'",
24        ));
25    }
26    decode_offer(trimmed).map_err(|e| Error::decode(format!("invalid offer: {e}")))
27}
28
29/// Parse a decoded [`SpendBundle`] into a spendable [`Offer`] within `ctx`.
30///
31/// The offer's parsed pointers (offered NFT metadata) are valid only for `ctx`'s allocator, so a
32/// build that reconstructs offered assets must use this SAME `ctx`.
33pub fn parse(ctx: &mut SpendContext, spend_bundle: &SpendBundle) -> Result<Offer> {
34    Offer::from_spend_bundle(ctx, spend_bundle)
35        .map_err(|e| Error::decode(format!("could not parse offer: {e}")))
36}
37
38#[cfg(test)]
39mod tests {
40    use super::*;
41
42    #[test]
43    fn decode_rejects_non_offer_prefix() {
44        let err = decode("hello world").unwrap_err();
45        assert!(matches!(&err, Error::Decode(m) if m.contains("not a Chia offer")));
46    }
47
48    #[test]
49    fn decode_rejects_blank_string() {
50        assert!(matches!(decode("   "), Err(Error::Decode(_))));
51    }
52
53    #[test]
54    fn decode_rejects_malformed_payload() {
55        // Correct prefix but a bad bech32 payload fails at decode, not with a panic.
56        assert!(matches!(decode("offer1qqzh3w"), Err(Error::Decode(_))));
57    }
58}