cotyledon 0.1.0

Framework for writing sprouts — sandboxed, event-driven on-chain trading bots.
Documentation
//! Runtime support for the macro-generated wasm entrypoint.
//!
//! `#[sprout::program]` expands to a single exported `__sprout_entry` that the
//! engine calls with `{"handler":"<name>","payload":<json>}`. That generated
//! code is intentionally thin — all the marshaling lives here and is referenced
//! as `cotyledon::__rt::*`. Compiled only for `wasm32`.
//!
//! ## Async model
//!
//! Host imports are synchronous from the guest's point of view: the engine
//! suspends the whole wasm fiber while it does the real (async) work, so a
//! handler's `.await` on a `Ctx` method resolves in a single poll. [`block_on`]
//! exploits that — it polls the handler future exactly once. A future that
//! returns `Pending` means the sprout awaited something other than a host call
//! (e.g. spun up its own async runtime), which is unsupported and traps.

use std::future::Future;
use std::task::{Context, Poll, RawWaker, RawWakerVTable, Waker};

use serde::de::DeserializeOwned;

use crate::host::abi;
use crate::{Error, Result};

/// An undecoded handler payload. Kept opaque so sprouts never name `serde_json`.
pub struct Payload(serde_json::Value);

/// Take ownership of the engine-written input buffer (and free it).
pub fn take_input(ptr: i32, len: i32) -> Vec<u8> {
    abi::read_owned(ptr, len)
}

/// Parse the `{handler, payload}` invocation envelope.
pub fn parse_invocation(bytes: &[u8]) -> Result<(String, Payload)> {
    let mut v: serde_json::Value =
        serde_json::from_slice(bytes).map_err(|e| Error::Codec(e.to_string()))?;
    let handler = v
        .get("handler")
        .and_then(|h| h.as_str())
        .ok_or_else(|| Error::Host("invocation missing handler".into()))?
        .to_owned();
    let payload = v
        .get_mut("payload")
        .map(serde_json::Value::take)
        .unwrap_or(serde_json::Value::Null);
    Ok((handler, Payload(payload)))
}

/// Decode a payload into the handler's argument type.
pub fn from_payload<T: DeserializeOwned>(payload: Payload) -> Result<T> {
    serde_json::from_value(payload.0).map_err(|e| Error::Codec(e.to_string()))
}

/// Error for an invocation naming a handler the program doesn't define.
pub fn unknown_handler(name: &str) -> Error {
    Error::Host(format!("unknown handler: {name}"))
}

/// Drive a handler future to completion under the one-poll model.
pub fn block_on<F: Future<Output = Result<()>>>(fut: F) -> Result<()> {
    fn clone(_: *const ()) -> RawWaker {
        RawWaker::new(std::ptr::null(), &VTABLE)
    }
    fn noop(_: *const ()) {}
    static VTABLE: RawWakerVTable = RawWakerVTable::new(clone, noop, noop, noop);

    let waker = unsafe { Waker::from_raw(RawWaker::new(std::ptr::null(), &VTABLE)) };
    let mut cx = Context::from_waker(&waker);
    let mut fut = fut;
    // SAFETY: `fut` is pinned to this stack frame and never moved afterwards.
    let mut fut = unsafe { std::pin::Pin::new_unchecked(&mut fut) };
    match fut.as_mut().poll(&mut cx) {
        Poll::Ready(out) => out,
        Poll::Pending => {
            panic!("sprout handler awaited a non-host future; sprouts may not run their own async runtime")
        }
    }
}

/// Encode a handler result as a response envelope, write it into a freshly
/// allocated guest buffer, and return its fat pointer for the engine to read.
pub fn finish(result: Result<()>) -> i64 {
    let env = match result {
        Ok(()) => serde_json::json!({ "ok": true, "value": null }),
        Err(e) => {
            let (kind, message) = match e {
                Error::Host(m) => ("Host", m),
                Error::Codec(m) => ("Codec", m),
                Error::Denied(m) => ("Denied", m),
            };
            serde_json::json!({ "ok": false, "error": { "kind": kind, "message": message } })
        }
    };
    let bytes = serde_json::to_vec(&env).unwrap_or_else(|_| b"{\"ok\":false}".to_vec());
    let len = bytes.len() as i32;
    let ptr = abi::__cotyledon_alloc(len);
    // SAFETY: `ptr` addresses `len` freshly allocated bytes.
    unsafe { std::ptr::copy_nonoverlapping(bytes.as_ptr(), ptr as *mut u8, bytes.len()) };
    abi::pack(ptr, len)
}