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}