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")
);
}
}