upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
path: root/5C.md
blob: 218229ca8432c8dbef28fabdc1573780d0206a93 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
# NIP-5C

## Scrolls

`draft` `optional`

This NIP defines a standard for publishing self-contained WebAssembly programs as Nostr events ("scrolls"). Scrolls are composed of basic metadata (`name`, `description` and `icon`) and initial execution parameters defined as tags and a WASM binary, base64-encoded as the content. The binaries are executed in a sandbox inside a "host" (i.e. a proper Nostr client). Scrolls interact with Nostr only through a set of simple APIs provided by the host.

---

## Event Format

```yaml
{
  "kind": 1227,
  "content": "<base64-encoded WASM binary>",
  "tags":
    [
      ["name", "<name>"],
      ["description", "<description>"],
      ["icon", "<image-url>"],
      ["param", "<... (see below)>"]
    ]
}
```

## Favorite scrolls list

Clients can publish a list of favorite scrolls using a NIP-51 standard list event with kind `10027`. The list should include one `"e"` tag per scroll event, with optional relay hints and author hints.

## String Convention

Unless noted otherwise (i.e. for hex pubkeys and ids), the module passes strings to the host as `(ptr: i32, len: i32)` pairs into its linear memory. The host returns strings (and other variable-length data) by returning an `i32` pointer to a buffer in linear memory whose first 4 bytes are the `u32` byte-length of the payload that follows.

For example, if `nostr.get_content(handle)` returns `7165` that will be a pointer to a buffer at location `7165`. If that buffer starts with `[ 00 00 00 03 ]` that means it contains 3 bytes (aside from the initial four), so we can keep reading it `[ 00 00 00 03 66 6f 6f ]` and conclude that the string is `"foo"`.

## Numbers Convention

All numbers written by the host must use **Little-endian** byte order, matching WASM's default endianness. This applies consistently: write string length prefixes as Little-endian numbers, write number parameters as Little-endian

## Memory Management Convention

In order to keep the size of WASM programs small (<10kb) we recommend not including any type of garbage collector or complex allocator. Just use the simplest possible bump allocator and do not free any memory -- programs will not use a lot of memory anyway, at most they will receive a bunch of `i32` event handles from the host, which are cheap.

Because of this the WASM program is expected to expose just a simple `alloc()` function to the host, which is used for the host to know a place it can write things to. No function capable of freeing memory is expected.

Hosts are encouraged to enforce resource limits for scroll execution, including memory consumption, number of open handles, and CPU usage.

---

## Global Exports

### `run(params_ptr: i32)`

The WASM module must export a function named `run` with a single pointer parameter. The client calls this on launch.

### `on_event(sub_handle: i32, event_handle: i32, eosed: i32)`

If the WASM module ever calls `nostr.subscribe` it must also export a function named `on_event` that will be called with every received event from any subscription. `sub` will be the subscription handle, `event` will be the event handle, `eosed` will be `0` if the event was received before `EOSE`, `1` otherwise.

### `on_eose(sub_handle: i32)`

Likewise, this will be called by the host whenever a subscription sends an `EOSE`.

### `alloc(size: i32) -> i32`

This function should allocate a buffer of the given size and return a pointer to it the host can write to.

---

## Global Imports

The WASM module **must** export its linear memory so the host can read guest strings and write return buffers:

```wat
(memory (export "memory") 1)
```

---

## Parameters

Scrolls can declare parameters that the host must provide when calling `run()`.

The tag format is `["param", "<name>", "<description>", "<type>", "<required>"]` where:

- `<name>` is the parameter identifier (for UI purposes).
- `<description>` is an optional human-readable description, can be left empty.
- `<type>` is one of: `public_key`, `event`, `string`, `number`, `timestamp`, `relay`.
- `<required>` is `"required"` if the parameter is required, empty otherwise.

The client running the program may prompt the user to provide these parameters or it may take them from the context automatically (for example, by providing the option of running a program that takes an `event` param from a context-menu on such an event). A simple program will probably use between 0 and 2 parameters usually.

A special parameter called `"me"` of type `public_key` can be specified to be automatically filled with the public key of the current user.

Parameters of type `event` may take an extra tag item `<supported_kinds>` after `<required>` with a comma-separated list of kinds that are acceptable.

### Example

For example, this scroll could be displayed as an option to be clicked on any user profile.

```yaml
{
  "kind": 1227,
  "content": "<base64-encoded WASM binary>",
  "tags":
    [
      ["name", "interactions"],
      ["description", "dispĺays interactions between me and someone else"],
      ["param", "me", "myself", "public_key", "required"],
      [
        "param",
        "public_key",
        "person with whom I want to see my interactions",
        "public_key",
        "required"
      ]
    ]
}
```

### Parameter Passing

When the host calls `run()`, it passes a single pointer to a buffer containing all parameters in the order they appear in the tags. Each type has a specific length or a way to determine its length (for strings, as described in "String Convention", this is a 4-byte prefix).

The host must prefix every parameter with a single presence byte: `1` indicates the parameter is provided, and `0` indicates it is omitted. If a required parameter is prefixed with `0`, the program must panic.

| Type         | Encoding                           | Size (if provided) |
| ------------ | -----------------------------------| ------------------ |
| `public_key` | 32 bytes                           | 32 bytes           |
| `event`      | i32 handle                         | 4 bytes            |
| `string`     | u32 length followed by UTF-8 bytes | 4 + len bytes      |
| `number`     | i32                                | 4 bytes            |
| `timestamp`  | Unix timestamp as u32              | 4 bytes            |
| `relay`      | Relay URL, same encoding as string | 4 + len bytes      |


For example, in a scroll with these parameters:

```json
[
    ["param", "me", "", "public_key", "required"],
    ["param", "note", "", "event", "required", "1,1111"],
    ["param", "author", "", "public_key", ""],
    ["param", "place", "", "relay", "required"]
]
```

The buffer layout would be:

```text
[ 1 ][ 32-byte pubkey ][ 1 ][ 4-byte handle ][ 0 ][ 1 ][ 4-byte len ][ UTF-8 relay URL ]
```

Because `author` is optional and not provided, its position contains only the `0` presence byte.

---

## Host Provided API

These are the functions the host must expose to be called by the WASM program.

Many of these expect and return handles. Handles are just `i32` numbers. The idea of dealing with handles and providing accessor functions is that the WASM programs don't have to include JSON parsers or any other form of encoder or decoder that would be necessary in order to send and receive structured data to and from the host. Instead they just deal with handles and request data to be extracted from these handles on their behalf to the host. This allows WASM programs to remain small and easy to write.

### `nostr.req_new() -> i32`

Create a new empty request handle.

### `nostr.req_add_author(req: i32, pubkey_ptr: i32) -> void`

Add a pubkey to the `authors` filter. It must be a pointer to a 32-byte buffer.

### `nostr.req_add_author_hex(req: i32, pubkey_hex_ptr: i32) -> void`

Same as `req_add_author` but with a hex-encoded pubkey string. Length is assumed to be 64 characters.

### `nostr.req_add_id(req: i32, id_ptr: i32) -> void`

Add an id to the `ids` filter. It must be a pointer to a 32-byte buffer.

### `nostr.req_add_id_hex(req: i32, id_hex_ptr: i32) -> void`

Same as `req_add_id` but with a hex-encoded id string. Length is assumed to be 64 characters.

### `nostr.req_add_kind(req: i32, kind: i32) -> void`

Add a kind integer to the `kinds` filter.

### `nostr.req_add_tag(req: i32, tag: i32, value_ptr: i32, value_len: i32) -> void`

Add a value to a tag filter. `tag` is the ASCII code of the letter (e.g. `112` is `"p"` and will be added to the filter as `"#p"`); `value_ptr/value_len` points to the tag value to match. The value is treated as a string.

### `nostr.req_add_tag_bin32(req: i32, tag: i32, value_ptr: i32) -> void`

Same as `req_add_tag` but the value is a pointer to the 32-byte binary buffer that the host will convert to hex. This is useful for `#p`, `#e`, and other tag filters that need pubkey or event ID values.

Hosts may deduplicate filter values (authors, ids, kinds, and tag values) when the module adds the same value more than once via `req_add_*`.

### `nostr.req_set_limit(req: i32, limit: i32) -> void`

Sets the `"limit"` attribute of the filter.

### `nostr.req_set_since(req: i32, timestamp: i32) -> void`

Sets the `"since"` attribute of the filter.

### `nostr.req_set_until(req: i32, timestamp: i32) -> void`

Sets the `"until"` attribute of the filter.

### `nostr.req_set_search(req: i32, ptr: i32, len: i32) -> void`

Sets the `"search"` attribute of the filter.

### `nostr.req_add_relay(req: i32, ptr: i32, len: i32) -> void`

Explicitly target a relay URL for this request.

If no relays are added to a request, the client should apply the following relay selection heuristics:

- If `authors` are specified, use the NIP-65 **write** relays of those pubkeys.
- If `#p` or `#P` tag filters are specified, use the NIP-65 **read** relays of those pubkeys.
- Otherwise, use an arbitrary set of relays or just fail.

### `nostr.req_close_on_eose(req: i32) -> void`

Mark this request to automatically close after `EOSE`.

### `nostr.subscribe(req: i32) -> i32`

Sends the REQ to the target relays and returns a subscription handle. The request handle is consumed by this call — do not drop it separately.

The globally exported `on_event` function will be called with the results.

- The module must call `nostr.drop(event)` on each non-zero event handle when done with it.
- If `nostr.req_close_on_eose` was set, the host drops the subscription handle after the EOSE callback. The module must not drop it.

To cancel a live subscription call `nostr.drop(sub)`.

### `nostr.event_get_id(event_handle: i32) -> i32`

Returns a pointer to a 32-byte buffer containing the event ID.

### `nostr.event_get_id_hex(event_handle: i32) -> i32`

Returns a pointer to a 64-character string containing the hex event ID.

### `nostr.event_get_pubkey(event_handle: i32) -> i32`

Returns a pointer to a 32-byte buffer containing the public key of the event author.

### `nostr.event_get_pubkey_hex(event_handle: i32) -> i32`

Returns a pointer to a 64-character string containing the hex public key of the event author.

### `nostr.event_get_kind(event_handle: i32) -> i32`

Returns the kind integer directly.

### `nostr.event_get_created_at(event_handle: i32) -> i32`

Returns the unix timestamp directly.

### `nostr.event_get_content(event_handle: i32) -> i32`

Returns a pointer to a buffer containing the event content (follows the String Convention above).

### `nostr.event_get_tag_count(event_handle: i32) -> i32`

Returns the total number of tags on this event.

### `nostr.event_get_tag_item_count(event_handle: i32, tag_index: i32) -> i32`

Returns the number of items in the tag at `tag_index`; or `0` if such tag doesn't exist.

### `nostr.event_get_tag_item(event_handle: i32, tag_index: i32, item_index: i32) -> i32`

Returns a pointer to a buffer containing the item at `(tag_index, item_index)` (follows the String Convention above); or `0` if such tag doesn't exist.

### `nostr.event_get_tag_item_bin32(event_handle: i32, tag_index: i32, item_index: i32) -> i32`

Same as `event_get_tag_item`, but returns a pointer to a 32-byte buffer of the item if it happened to be a pubkey or an event id; `0` otherwise.

### `nostr.event_get_tag_item_by_name(event_handle: i32, name_ptr: i32, name_len: i32, item_index: i32) -> i32`

Finds the first tag whose name (item 0) matches the string at `name_ptr/name_len`, then returns a pointer to a buffer containing item `item_index` from that tag; `0` if no matching tag is found.

### `nostr.event_get_tag_item_by_name_bin32(event_handle: i32, name_ptr: i32, name_len: i32, item_index: i32) -> i32`

Same as `event_get_tag_item_by_name`, but returns a pointer to a 32-byte buffer of the value if it happened to be a pubkey or an event id; `0` otherwise.

### `nostr.display(event: i32) -> void`

Render an event through the client's native note renderer. The event handle is not consumed.

### `nostr.log(ptr: i32, len: i32) -> void`

Emit a log message to the host's debug console or developer tooling. The string at `ptr/len` is the message.

### `nostr.drop(handle: i32) -> void`

Releases any handle: unconsumed request, active subscription (cancels it), individual event.

---

## Example programs and host code

- JavaScript host runtime example: https://viewsource.win/fiatjaf.com/nprogram/_/master/~/src/nprogram-host.ts
- Rust example (basic, just reads from a relay): https://viewsource.win/fiatjaf.com/nprogram/_/master/~/examples/basic/src/lib.rs
- Zig example (displays the history of all interactions between the current user and some other user): https://viewsource.win/fiatjaf.com/nprogram/_/master/~/examples/interactions/main.zig
- TinyGo example (fetches all notifications, only display the unreplied ones): https://viewsource.win/fiatjaf.com/nprogram/_/master/~/examples/unreplied/main.go
- AssemblyScript example (fetches events from multiple relays and does a diff of the content): https://viewsource.win/fiatjaf.com/nprogram/_/master/~/examples/old-trending/main.ts