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