Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/holger

holger-estate-front/src/shop_door.rs

//! **`ShopDoor`: a sold Holger estate's signed `SoldEstate` client** (feature `metered`).
//!
//! A sold Holger runs NO monetize of its own (G16): its boxes pull from the ONE monetize
//! (holger-fleet on the shared pump), and this front calls the ONE monetize's `SoldEstate` door
//! for what only the shop can do for it. Since P4a (2026-10-25) the client is SHARED:
//! monetize-estate-side's `sold_door` (the door from the feed, the bench seam, every call signed
//! by the estate's client key, its bounds, the update step's `Shop`), moved there from this file;
//! Gunnar's front takes it in T16. What stays Holger's is what this front does with the shop's
//! installer material:
//!
//! | call | what |
//! |---|---|
//! | `TakeMaterial` | the installer material the shop's pairing left, kept in the primary's sealed slot ([`crate::sealed`]), then `done` ([`keep_material`]) |
//! | `Recharge` | one LOST box laid again with a fresh seed, then `done` ([`crate::recharge`]) |
//! | `Upgrade` | one standing box booted once from the release's keep medium (data kept), then `done` |
//! | `UpdateTake` / `UpdateReport` | the update the operator opened, pulled box by box ([`crate::update`]) |

pub use monetize_estate_side::sold_door::*;

/// **The bundle this front took, held in memory** (F9, Gunnar's front `hold`): what it kept in
/// the primary's slot, or took and could not keep there yet — the update and the renewals read
/// it while the slot cannot answer.
static HELD: std::sync::Mutex<Option<Vec<u8>>> = std::sync::Mutex::new(None);

/// The bundle this front holds ([`HELD`]), if any.
pub fn held() -> Option<Vec<u8>> {
    HELD.lock().unwrap_or_else(|p| p.into_inner()).clone()
}

fn hold(bundle: &[u8]) {
    *HELD.lock().unwrap_or_else(|p| p.into_inner()) = Some(bundle.to_vec());
}

/// **Keep the estate's installer material**: in the primary's sealed slot already → nothing;
/// else taken from the shop, proven a kept bundle, kept, held, then `done` (only after it is
/// kept may the shop forget it) — Gunnar's front `take_kept`. Every take is said at WARN (lane
/// F9: test3-holger-rs's r6 front tried every two minutes and said nothing): the call, the
/// shop's answer or refusal, the store and the `done`.
pub fn keep_material(door: &ShopDoor, slot: &crate::sealed::PrimarySlot) -> Result<String, String> {
    take_kept(door, slot, &|b| slot.put(crate::sealed::MATERIAL_OBJECT, b))
}

/// [`keep_material`] over any store (a test's).
pub fn take_kept(
    door: &dyn Take,
    slot: &crate::sealed::PrimarySlot,
    store: &dyn Fn(&[u8]) -> Result<(), String>,
) -> Result<String, String> {
    match slot.get(crate::sealed::MATERIAL_OBJECT) {
        Ok(Some(b)) if !b.is_empty() => {
            hold(&b);
            return Ok(format!(
                "the installer material is kept in {}",
                slot.describe()
            ));
        }
        Ok(_) => {
            tracing::warn!(slot = %slot.describe(), "material: the primary keeps no installer material: the material ask asked")
        }
        Err(e) => {
            tracing::warn!(slot = %slot.describe(), error = %e, "material: the primary's sealed slot cannot be read: not taken");
            return Err(format!("the primary's sealed slot: {e}"));
        }
    }
    take_and_store(door, &slot.describe(), store)
}

/// The shop's `the material ask`, as [`take_kept`] calls it.
pub trait Take {
    /// The bundle and the shop's words, or with `done` only its words.
    fn take(&self, done: bool) -> Result<(Vec<u8>, String), String>;
}

impl Take for ShopDoor {
    fn take(&self, done: bool) -> Result<(Vec<u8>, String), String> {
        self.take_material(done).map(|r| (r.bundle, r.words))
    }
}

/// Pure over `door` and `store`: **take, prove, keep, hold, `done`** — every step said at WARN.
pub fn take_and_store(
    door: &dyn Take,
    slot: &str,
    store: &dyn Fn(&[u8]) -> Result<(), String>,
) -> Result<String, String> {
    let (bundle, words) = match door.take(false) {
        Ok(t) => t,
        Err(e) => {
            tracing::warn!(error = %e, "material: the material ask was answered with a refusal");
            return Err(format!("the shop: {e}"));
        }
    };
    tracing::warn!(bytes = bundle.len(), words = %words, "material: the material ask answered");
    if bundle.is_empty() {
        return Err(format!("the shop handed no material: {words}"));
    }
    if let Err(e) = nordisk_pairing::kept::Kept::parse(&bundle) {
        tracing::warn!(error = %e, "material: the shop's answer is no kept bundle: not stored");
        return Err(format!("the shop's installer material: {e}"));
    }
    hold(&bundle);
    if let Err(e) = store(&bundle) {
        tracing::warn!(slot = %slot, error = %e, "material: the bundle could not be kept in the primary's sealed slot (held here; the shop is not told done)");
        return Err(format!(
            "the installer material could not be kept in {slot}: {e}"
        ));
    }
    let done = match door.take(true) {
        Ok((_, w)) => w,
        Err(e) => {
            tracing::warn!(error = %e, "material: kept, but the material ask (done) was refused: asked again");
            return Err(format!("kept in {slot}, but the shop's done: {e}"));
        }
    };
    let said = format!(
        "the installer material ({} bytes) is kept in {slot}; the shop: {done}",
        bundle.len()
    );
    tracing::warn!("material: {said}");
    Ok(said)
}

#[cfg(test)]
mod tests {
    use super::*;

    struct Shop(Result<Vec<u8>, String>, std::sync::Mutex<Vec<bool>>);

    impl Take for Shop {
        fn take(&self, done: bool) -> Result<(Vec<u8>, String), String> {
            self.1.lock().unwrap().push(done);
            self.0.clone().map(|b| (b, "handed".to_string()))
        }
    }

    /// ★ F9: **a refusal, or an answer that is no kept bundle, stores nothing and tells the
    /// shop no `done`** — and each is said (the words name the shop's refusal).
    #[test]
    fn a_refused_or_junk_take_stores_nothing_and_tells_the_shop_no_done() {
        let refused = Shop(
            Err("refused: acme is not delivered (laying)".into()),
            Default::default(),
        );
        let e = take_and_store(&refused, "the slot", &|_| panic!("never stored")).unwrap_err();
        assert!(
            e.contains("the shop: refused: acme is not delivered"),
            "{e}"
        );
        assert_eq!(*refused.1.lock().unwrap(), vec![false]);
        let junk = Shop(Ok(b"{\"v\":\"other\"}".to_vec()), Default::default());
        let e = take_and_store(&junk, "the slot", &|_| panic!("never stored")).unwrap_err();
        assert!(e.contains("the shop's installer material"), "{e}");
        assert_eq!(
            *junk.1.lock().unwrap(),
            vec![false],
            "no done for a bundle never kept"
        );
        let empty = Shop(Ok(Vec::new()), Default::default());
        assert!(
            take_and_store(&empty, "the slot", &|_| panic!("never stored"))
                .unwrap_err()
                .contains("handed no material")
        );
    }
}