Skip to main content

rpc/natives/
template.rs

1//! Render-only draft natives.
2//!
3//! These host fns take no database action; each mutates the
4//! [`TransactionDraft`](crate::draft::TransactionDraft) held in the session's
5//! Store data, which the render entry point reads back via `store.into_data()`.
6//!
7//! They are *functionally* render-only: the linker binds them on BOTH the
8//! normal eval channel ([`super::link`]) and the render surface
9//! ([`super::link_render`]) — the import must resolve wherever a compiled module
10//! references it — but the draft accumulator is armed only under render
11//! ([`SessionData::for_render`]). On the normal channel `with_draft` finds no
12//! draft and traps, so a non-render program can compile/link a draft native yet
13//! never accumulate anything. That trap is a safety net, not the boundary; the
14//! real gate is the render compiler-spec allowlist (a normal template/eval that
15//! shouldn't see these simply doesn't have them in scope). They are harmless on
16//! the normal channel either way: no DB, no secret, no mutation.
17//!
18//! `draft-split` takes the entity refs returned by `get-account` /
19//! `get-commodity` plus a rational amount; it reads each entity's `id` field
20//! (slot 0, per `ENTITY_LAYOUTS`) host-side so the draft records stable uuids.
21
22use nomiscript::EntityKind;
23use scripting::runtime::{read_entity_string_field, read_ratio_arg, read_string_arg};
24use wasmtime::{ArrayRef, Caller, Linker, Rooted, StructRef};
25
26use crate::draft::{DraftSplit, DraftTag};
27use crate::session::SessionData;
28
29pub const REGISTERED_COMMANDS: &[&str] = &[
30    "set-draft-note",
31    "set-draft-date",
32    "draft-split",
33    "draft-tag",
34    "draft-split-tag",
35];
36
37type StringArg = Option<Rooted<ArrayRef>>;
38type StructArg = Option<Rooted<StructRef>>;
39
40type Fut<'a, O> = Box<dyn std::future::Future<Output = wasmtime::Result<O>> + Send + 'a>;
41
42pub fn register(linker: &mut Linker<SessionData>) -> wasmtime::Result<()> {
43    linker.func_wrap_async(
44        "nomi",
45        "template_set_draft_note",
46        |mut caller: Caller<'_, SessionData>, (note_arg,): (StringArg,)| -> Fut<'_, i32> {
47            Box::new(async move {
48                let note = read_string_arg(&mut caller, note_arg)?
49                    .ok_or_else(|| wasmtime::Error::msg("set-draft-note: missing note"))?;
50                caller.data().with_draft(|d| d.set_note(note))?;
51                Ok(1)
52            })
53        },
54    )?;
55    linker.func_wrap_async(
56        "nomi",
57        "template_set_draft_date",
58        |mut caller: Caller<'_, SessionData>, (date_arg,): (StringArg,)| -> Fut<'_, i32> {
59            Box::new(async move {
60                let date = read_string_arg(&mut caller, date_arg)?
61                    .ok_or_else(|| wasmtime::Error::msg("set-draft-date: missing date"))?;
62                caller.data().with_draft(|d| d.set_date(date))?;
63                Ok(1)
64            })
65        },
66    )?;
67    linker.func_wrap_async(
68        "nomi",
69        "template_draft_split",
70        |mut caller: Caller<'_, SessionData>,
71         (account_arg, commodity_arg, amount_arg): (StructArg, StructArg, StructArg)|
72         -> Fut<'_, i32> {
73            Box::new(async move {
74                let account_id =
75                    read_entity_string_field(&mut caller, account_arg, EntityKind::Account, "id")?
76                        .ok_or_else(|| wasmtime::Error::msg("draft-split: missing account"))?;
77                let commodity_id = read_entity_string_field(
78                    &mut caller,
79                    commodity_arg,
80                    EntityKind::Commodity,
81                    "id",
82                )?
83                .ok_or_else(|| wasmtime::Error::msg("draft-split: missing commodity"))?;
84                let (value_num, value_denom) = read_ratio_arg(&mut caller, amount_arg)?
85                    .ok_or_else(|| wasmtime::Error::msg("draft-split: missing amount"))?;
86                // Return the split's index as the handle a template passes to
87                // `draft-split-tag` to tag this specific split. Reject the absurd
88                // >i32::MAX case BEFORE inserting so a failed call never leaves a
89                // partially-mutated draft (and a stale handle can never silently
90                // address the wrong split).
91                let mut index: Option<usize> = None;
92                caller.data().with_draft(|d| {
93                    if i32::try_from(d.split_count()).is_ok() {
94                        index = Some(d.add_split(DraftSplit {
95                            account_id,
96                            commodity_id,
97                            value_num,
98                            value_denom,
99                            tags: Vec::new(),
100                        }));
101                    }
102                })?;
103                let index =
104                    index.ok_or_else(|| wasmtime::Error::msg("draft-split: too many splits"))?;
105                i32::try_from(index)
106                    .map_err(|_| wasmtime::Error::msg("draft-split: too many splits"))
107            })
108        },
109    )?;
110    linker.func_wrap_async(
111        "nomi",
112        "template_draft_tag",
113        |mut caller: Caller<'_, SessionData>,
114         (name_arg, value_arg): (StringArg, StringArg)|
115         -> Fut<'_, i32> {
116            Box::new(async move {
117                let name = read_string_arg(&mut caller, name_arg)?
118                    .ok_or_else(|| wasmtime::Error::msg("draft-tag: missing name"))?;
119                let value = read_string_arg(&mut caller, value_arg)?
120                    .ok_or_else(|| wasmtime::Error::msg("draft-tag: missing value"))?;
121                caller
122                    .data()
123                    .with_draft(|d| d.add_tag(DraftTag { name, value }))?;
124                Ok(1)
125            })
126        },
127    )?;
128    linker.func_wrap_async(
129        "nomi",
130        "template_draft_split_tag",
131        |mut caller: Caller<'_, SessionData>,
132         (index_arg, name_arg, value_arg): (i32, StringArg, StringArg)|
133         -> Fut<'_, i32> {
134            Box::new(async move {
135                let index = usize::try_from(index_arg).map_err(|_| {
136                    wasmtime::Error::msg("draft-split-tag: split index must be non-negative")
137                })?;
138                let name = read_string_arg(&mut caller, name_arg)?
139                    .ok_or_else(|| wasmtime::Error::msg("draft-split-tag: missing name"))?;
140                let value = read_string_arg(&mut caller, value_arg)?
141                    .ok_or_else(|| wasmtime::Error::msg("draft-split-tag: missing value"))?;
142                let mut ok = false;
143                caller
144                    .data()
145                    .with_draft(|d| ok = d.add_split_tag(index, DraftTag { name, value }))?;
146                if !ok {
147                    return Err(wasmtime::Error::msg(format!(
148                        "draft-split-tag: no draft split at index {index} \
149                         (pass the value returned by draft-split)"
150                    )));
151                }
152                Ok(1)
153            })
154        },
155    )?;
156    Ok(())
157}