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

nordisk/monetize

guests/toy-vendor/src/lib.rs

//! **A toy payment vendor, as a guest.** No money and no vendor behind it: a
//! loopback HTTP server stands in for the vendor's API, and every answer is
//! what that server says. It exists so the host's tests can prove, with a real
//! `.wasm`, that
//!
//! * the credential is added by the HOST (this guest sends a forged
//!   `Authorization` of its own, and the server must see the real one);
//! * a webhook is verified by the HOST's `hmac` door, against a secret this
//!   guest never holds;
//! * the host's memory outlives a call (and a live replacement);
//! * every vendor op crosses the ABI with its error arm intact.

use monetize_pay_guest::monetize_pay::payment::{Charge, Of, Page, PayManifest, Return, Standing};
use monetize_pay_guest::monetize_pay::{
    CallbackEvent, ChargeHandle, ChargeRequest, ChargeStatus, Market, Money, PayError, RawCallback,
    Settlement,
};
use monetize_pay_guest::{kinds, Host, Manifest, VendorGuest};

pub const ID: &str = "toyvendor";

/// What the toy forges, to prove the host overrides it.
pub const FORGED: &str = "Bearer forged-by-the-guest";

#[derive(Default)]
pub struct ToyVendor;

fn base(s: &serde_json::Value) -> Result<String, PayError> {
    s["base"]
        .as_str()
        .map(|b| b.trim_end_matches('/').to_string())
        .ok_or_else(|| PayError::Unavailable("toyvendor: no `base` setting".into()))
}

fn http(
    host: &dyn Host,
    method: &str,
    url: String,
    body: Vec<u8>,
) -> Result<(u16, serde_json::Value), PayError> {
    let r = host
        .http(
            &monetize_pay_guest::__reexport::monetize_product_guest::wire::HttpRequest {
                method: method.into(),
                url,
                headers: vec![
                    ("authorization".into(), FORGED.into()),
                    ("content-type".into(), "application/json".into()),
                ],
                body,
            },
        )
        .map_err(|e| PayError::Unavailable(format!("toyvendor: {e}")))?;
    Ok((
        r.status,
        serde_json::from_slice(&r.body).unwrap_or(serde_json::Value::Null),
    ))
}

fn status_of(j: &serde_json::Value) -> ChargeStatus {
    match j["status"].as_str().unwrap_or("") {
        "captured" => ChargeStatus::Captured {
            at_unix_ms: j["at"].as_u64().unwrap_or(0),
            vendor_ref: j["ref"].as_str().unwrap_or("").into(),
        },
        "released" => ChargeStatus::Released,
        "held" => ChargeStatus::Reserved,
        _ => ChargeStatus::AwaitingCustomer,
    }
}

fn reserve(
    host: &dyn Host,
    s: &serde_json::Value,
    req: &ChargeRequest,
) -> Result<ChargeHandle, PayError> {
    let body = serde_json::to_vec(req).unwrap_or_default();
    let (code, j) = http(host, "POST", format!("{}/reserve", base(s)?), body)?;
    if code == 402 {
        return Err(PayError::Declined(format!(
            "toyvendor: {}",
            j["error"].as_str().unwrap_or("declined")
        )));
    }
    let id = j["id"]
        .as_str()
        .ok_or_else(|| PayError::Unavailable(format!("toyvendor: reserve -> {code}")))?
        .to_string();
    // The host's memory: which reference this handle is for.
    host.state_put(&format!("ref:{id}"), req.reference.as_bytes())
        .map_err(PayError::Unavailable)?;
    Ok(ChargeHandle {
        vendor: ID.into(),
        id,
    })
}

impl VendorGuest for ToyVendor {
    fn manifest(&self) -> Manifest {
        Manifest::of_kind(kinds::VENDOR, ID, &["http", "hmac", "state"])
    }
    /// The block must name the vendor's `base`; a refusal keeps it out at load.
    fn pay_manifest(
        &self,
        _: &dyn Host,
        settings: &serde_json::Value,
    ) -> Result<PayManifest, String> {
        base(settings).map_err(|e| e.to_string())?;
        Ok(PayManifest {
            vendor: ID.into(),
            markets: vec![Market::Se],
            settings: vec!["base".into()],
            holds: true,
            test_payer: None,
        })
    }
    /// An order's page at the vendor; the toy sells no sign-up, card check or test sign-up page.
    fn page(
        &self,
        host: &dyn Host,
        s: &serde_json::Value,
        charge: &Charge<'_>,
        _: &Return,
    ) -> Result<Page, PayError> {
        Ok(match charge {
            Charge::Order(req) => Page::Order {
                handle: reserve(host, s, req)?,
                url: None,
            },
            Charge::Signup(_) => Page::Signup(None),
            Charge::CardCheck(_) => Page::CardCheck(None),
            Charge::TestSignup(_) => Page::TestSignup(None),
        })
    }
    fn status(
        &self,
        host: &dyn Host,
        s: &serde_json::Value,
        of: &Of<'_>,
    ) -> Result<Standing, PayError> {
        Ok(match of {
            Of::Charge(h) => {
                let (code, j) = http(
                    host,
                    "GET",
                    format!("{}/status/{}", base(s)?, h.id),
                    Vec::new(),
                )?;
                if code == 404 {
                    return Err(PayError::UnknownHandle((*h).clone()));
                }
                Standing::Charge(status_of(&j))
            }
            Of::Page(h) => Standing::Page(base(s).ok().map(|b| format!("{b}/pay/{}", h.id))),
            Of::Signup { .. } => Standing::Signup(None),
            Of::CardCheck { .. } => Standing::CardCheck(None),
        })
    }
    fn capture(
        &self,
        host: &dyn Host,
        s: &serde_json::Value,
        h: &ChargeHandle,
    ) -> Result<ChargeStatus, PayError> {
        let (code, j) = http(
            host,
            "POST",
            format!("{}/capture/{}", base(s)?, h.id),
            Vec::new(),
        )?;
        if code == 404 {
            return Err(PayError::UnknownHandle(h.clone()));
        }
        Ok(status_of(&j))
    }
    fn release(
        &self,
        host: &dyn Host,
        s: &serde_json::Value,
        h: &ChargeHandle,
    ) -> Result<ChargeStatus, PayError> {
        let (code, j) = http(
            host,
            "POST",
            format!("{}/release/{}", base(s)?, h.id),
            Vec::new(),
        )?;
        if code == 404 {
            return Err(PayError::UnknownHandle(h.clone()));
        }
        Ok(status_of(&j))
    }
    /// `toy-signature: <hex hmac-sha256 of the body>`, checked by the HOST.
    fn callback(
        &self,
        host: &dyn Host,
        _: &serde_json::Value,
        raw: RawCallback<'_>,
    ) -> Result<CallbackEvent, PayError> {
        let sig = raw
            .headers
            .iter()
            .find(|(k, _)| k.eq_ignore_ascii_case("toy-signature"))
            .map(|(_, v)| v.clone())
            .ok_or_else(|| PayError::Unverified("toyvendor: no toy-signature".into()))?;
        match host.hmac_verify("toy_hook", raw.body, &[sig]) {
            Ok(true) => {}
            Ok(false) => return Err(PayError::Unverified("toyvendor: signature mismatch".into())),
            Err(e) => return Err(PayError::Unverified(format!("toyvendor: {e}"))),
        }
        let j: serde_json::Value = serde_json::from_slice(raw.body)
            .map_err(|e| PayError::Ignored(format!("toyvendor: {e}")))?;
        let id = j["id"].as_str().unwrap_or("").to_string();
        let reference = match host.state_get(&format!("ref:{id}")) {
            Ok(Some(r)) => String::from_utf8_lossy(&r).into_owned(),
            _ => return Err(PayError::Ignored(format!("toyvendor: {id} is not ours"))),
        };
        Ok(CallbackEvent {
            reference,
            handle: ChargeHandle {
                vendor: ID.into(),
                id,
            },
            status: status_of(&j),
        })
    }
    /// Asks the vendor (`POST /refund/<id>`, the amount as the body). A vendor that has no
    /// refund for it (no such route, nothing drawn) is a decline, named with its status.
    fn refund(
        &self,
        host: &dyn Host,
        s: &serde_json::Value,
        h: &ChargeHandle,
        amount: Money,
    ) -> Result<ChargeStatus, PayError> {
        let body = serde_json::to_vec(&amount).unwrap_or_default();
        let (code, j) = http(host, "POST", format!("{}/refund/{}", base(s)?, h.id), body)?;
        if code != 200 {
            return Err(PayError::Declined(format!(
                "toyvendor: the vendor refused the refund of {} ({code})",
                h.id
            )));
        }
        Ok(status_of(&j))
    }
    fn settlement(
        &self,
        _: &dyn Host,
        _: &serde_json::Value,
        _: &ChargeHandle,
    ) -> Option<Settlement> {
        None
    }
}

monetize_pay_guest::export_vendor!(ToyVendor, grants = [http, hmac, state]);