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