14 KiB
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
{
"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:
(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.
{
"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:
[
["param", "me", "", "public_key", "required"],
["param", "note", "", "event", "required", "1,1111"],
["param", "author", "", "public_key", ""],
["param", "place", "", "relay", "required"]
]
The buffer layout would be:
[ 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: u32) -> 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: u32) -> void
Sets the "limit" attribute of the filter.
nostr.req_set_since(req: i32, timestamp: u32) -> void
Sets the "since" attribute of the filter.
nostr.req_set_until(req: i32, timestamp: u32) -> 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
authorsare specified, use the NIP-65 write relays of those pubkeys. - If
#por#Ptag 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_eosewas 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) -> u32
Returns the kind integer directly.
nostr.event_get_created_at(event_handle: i32) -> u32
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) -> u32
Returns the total number of tags on this event.
nostr.event_get_tag_item_count(event_handle: i32, tag_index: u32) -> u32
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: u32, item_index: u32) -> 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: u32, item_index: u32) -> 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: u32) -> 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: u32) -> 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