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

            
22
use nomiscript::EntityKind;
23
use scripting::runtime::{read_entity_string_field, read_ratio_arg, read_string_arg};
24
use wasmtime::{ArrayRef, Caller, Linker, Rooted, StructRef};
25

            
26
use crate::draft::{DraftSplit, DraftTag};
27
use crate::session::SessionData;
28

            
29
pub const REGISTERED_COMMANDS: &[&str] = &[
30
    "set-draft-note",
31
    "set-draft-date",
32
    "draft-split",
33
    "draft-tag",
34
    "draft-split-tag",
35
];
36

            
37
type StringArg = Option<Rooted<ArrayRef>>;
38
type StructArg = Option<Rooted<StructRef>>;
39

            
40
type Fut<'a, O> = Box<dyn std::future::Future<Output = wasmtime::Result<O>> + Send + 'a>;
41

            
42
7572
pub fn register(linker: &mut Linker<SessionData>) -> wasmtime::Result<()> {
43
7572
    linker.func_wrap_async(
44
7572
        "nomi",
45
7572
        "template_set_draft_note",
46
26
        |mut caller: Caller<'_, SessionData>, (note_arg,): (StringArg,)| -> Fut<'_, i32> {
47
26
            Box::new(async move {
48
26
                let note = read_string_arg(&mut caller, note_arg)?
49
26
                    .ok_or_else(|| wasmtime::Error::msg("set-draft-note: missing note"))?;
50
26
                caller.data().with_draft(|d| d.set_note(note))?;
51
26
                Ok(1)
52
26
            })
53
26
        },
54
    )?;
55
7572
    linker.func_wrap_async(
56
7572
        "nomi",
57
7572
        "template_set_draft_date",
58
26
        |mut caller: Caller<'_, SessionData>, (date_arg,): (StringArg,)| -> Fut<'_, i32> {
59
26
            Box::new(async move {
60
26
                let date = read_string_arg(&mut caller, date_arg)?
61
26
                    .ok_or_else(|| wasmtime::Error::msg("set-draft-date: missing date"))?;
62
26
                caller.data().with_draft(|d| d.set_date(date))?;
63
26
                Ok(1)
64
26
            })
65
26
        },
66
    )?;
67
7572
    linker.func_wrap_async(
68
7572
        "nomi",
69
7572
        "template_draft_split",
70
        |mut caller: Caller<'_, SessionData>,
71
         (account_arg, commodity_arg, amount_arg): (StructArg, StructArg, StructArg)|
72
130
         -> Fut<'_, i32> {
73
130
            Box::new(async move {
74
104
                let account_id =
75
130
                    read_entity_string_field(&mut caller, account_arg, EntityKind::Account, "id")?
76
130
                        .ok_or_else(|| wasmtime::Error::msg("draft-split: missing account"))?;
77
104
                let commodity_id = read_entity_string_field(
78
104
                    &mut caller,
79
104
                    commodity_arg,
80
104
                    EntityKind::Commodity,
81
104
                    "id",
82
                )?
83
104
                .ok_or_else(|| wasmtime::Error::msg("draft-split: missing commodity"))?;
84
104
                let (value_num, value_denom) = read_ratio_arg(&mut caller, amount_arg)?
85
104
                    .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
104
                let mut index: Option<usize> = None;
92
104
                caller.data().with_draft(|d| {
93
104
                    if i32::try_from(d.split_count()).is_ok() {
94
104
                        index = Some(d.add_split(DraftSplit {
95
104
                            account_id,
96
104
                            commodity_id,
97
104
                            value_num,
98
104
                            value_denom,
99
104
                            tags: Vec::new(),
100
104
                        }));
101
104
                    }
102
104
                })?;
103
104
                let index =
104
104
                    index.ok_or_else(|| wasmtime::Error::msg("draft-split: too many splits"))?;
105
104
                i32::try_from(index)
106
104
                    .map_err(|_| wasmtime::Error::msg("draft-split: too many splits"))
107
130
            })
108
130
        },
109
    )?;
110
7572
    linker.func_wrap_async(
111
7572
        "nomi",
112
7572
        "template_draft_tag",
113
        |mut caller: Caller<'_, SessionData>,
114
         (name_arg, value_arg): (StringArg, StringArg)|
115
26
         -> Fut<'_, i32> {
116
26
            Box::new(async move {
117
26
                let name = read_string_arg(&mut caller, name_arg)?
118
26
                    .ok_or_else(|| wasmtime::Error::msg("draft-tag: missing name"))?;
119
26
                let value = read_string_arg(&mut caller, value_arg)?
120
26
                    .ok_or_else(|| wasmtime::Error::msg("draft-tag: missing value"))?;
121
26
                caller
122
26
                    .data()
123
26
                    .with_draft(|d| d.add_tag(DraftTag { name, value }))?;
124
26
                Ok(1)
125
26
            })
126
26
        },
127
    )?;
128
7572
    linker.func_wrap_async(
129
7572
        "nomi",
130
7572
        "template_draft_split_tag",
131
        |mut caller: Caller<'_, SessionData>,
132
         (index_arg, name_arg, value_arg): (i32, StringArg, StringArg)|
133
52
         -> Fut<'_, i32> {
134
52
            Box::new(async move {
135
52
                let index = usize::try_from(index_arg).map_err(|_| {
136
                    wasmtime::Error::msg("draft-split-tag: split index must be non-negative")
137
                })?;
138
52
                let name = read_string_arg(&mut caller, name_arg)?
139
52
                    .ok_or_else(|| wasmtime::Error::msg("draft-split-tag: missing name"))?;
140
52
                let value = read_string_arg(&mut caller, value_arg)?
141
52
                    .ok_or_else(|| wasmtime::Error::msg("draft-split-tag: missing value"))?;
142
52
                let mut ok = false;
143
52
                caller
144
52
                    .data()
145
52
                    .with_draft(|d| ok = d.add_split_tag(index, DraftTag { name, value }))?;
146
52
                if !ok {
147
26
                    return Err(wasmtime::Error::msg(format!(
148
26
                        "draft-split-tag: no draft split at index {index} \
149
26
                         (pass the value returned by draft-split)"
150
26
                    )));
151
26
                }
152
26
                Ok(1)
153
52
            })
154
52
        },
155
    )?;
156
7572
    Ok(())
157
7572
}