diff options
| -rw-r--r-- | A5.md | 288 |
1 files changed, 288 insertions, 0 deletions
| @@ -0,0 +1,288 @@ | |||
| 1 | # NIP-A5 | ||
| 2 | |||
| 3 | ## WASM Programs | ||
| 4 | |||
| 5 | `draft` `optional` | ||
| 6 | |||
| 7 | This NIP defines a standard for publishing self-contained WebAssembly programs as Nostr events. Programs are base64-encoded, executed client-side, and interact with Nostr exclusively through a host-provided handle-based API for building requests, querying relays, and rendering results. | ||
| 8 | |||
| 9 | --- | ||
| 10 | |||
| 11 | ## Event Format | ||
| 12 | |||
| 13 | ```yaml | ||
| 14 | { | ||
| 15 | "kind": 1227, | ||
| 16 | "content": "<base64-encoded WASM binary>", | ||
| 17 | "tags": | ||
| 18 | [ | ||
| 19 | ["name", "<program-name>"], | ||
| 20 | ["description", "<optional-description>"], | ||
| 21 | ["icon", "<image-url>"], | ||
| 22 | ["param", "<... (see below)>"] | ||
| 23 | ] | ||
| 24 | } | ||
| 25 | ``` | ||
| 26 | |||
| 27 | ## String Convention | ||
| 28 | |||
| 29 | Unless noted otherwise (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_be` byte-length of the payload that follows. | ||
| 30 | |||
| 31 | 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"`. | ||
| 32 | |||
| 33 | ## Memory Management Convention | ||
| 34 | |||
| 35 | 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. | ||
| 36 | |||
| 37 | 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. | ||
| 38 | |||
| 39 | --- | ||
| 40 | |||
| 41 | ## Global Exports | ||
| 42 | |||
| 43 | ### `run(params_ptr: i32)` | ||
| 44 | |||
| 45 | The WASM module must export a function named `run` with a single pointer parameter. The client calls this on launch. | ||
| 46 | |||
| 47 | ### `on_event(sub_handle: i32, event_handle: i32, eosed: i32)` | ||
| 48 | |||
| 49 | 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. | ||
| 50 | |||
| 51 | ### `on_eose(sub_handle: i32)` | ||
| 52 | |||
| 53 | Likewise, this will be called by the host whenever a subscription sends an `EOSE`. | ||
| 54 | |||
| 55 | ### `alloc(size: i32) -> i32` | ||
| 56 | |||
| 57 | This function should allocate a buffer of the given size and return a pointer to it the host can write to. | ||
| 58 | |||
| 59 | --- | ||
| 60 | |||
| 61 | ## Global Imports | ||
| 62 | |||
| 63 | The WASM module **must** export its linear memory so the host can read guest strings and write return buffers: | ||
| 64 | |||
| 65 | ```wat | ||
| 66 | (memory (export "memory") 1) | ||
| 67 | ``` | ||
| 68 | |||
| 69 | --- | ||
| 70 | |||
| 71 | ## Parameters | ||
| 72 | |||
| 73 | Programs can declare parameters that the host must provide when calling `run()`. Parameters are declared as tags on the program event. | ||
| 74 | |||
| 75 | The tag format is `["param", "<name>", "<description>", "<type>", "<required>"]` where: | ||
| 76 | |||
| 77 | - `<name>` is the parameter identifier (for UI purposes). | ||
| 78 | - `<description>` is an optional human-readable description, can be left empty. | ||
| 79 | - `<type>` is one of: `public_key`, `event`, `string`, `number`, `timestamp`, `relay`. | ||
| 80 | - `<required>` is `"required"` if the parameter is required, empty otherwise. | ||
| 81 | |||
| 82 | 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. | ||
| 83 | |||
| 84 | A special parameter called `"me"` of type `public_key` can be specified to be automatically filled with the public key of the current user. | ||
| 85 | |||
| 86 | Parameters of type `event` may take an extra tag item `<supported_kinds>` after `<required>` with a comma-separated list of kinds that are acceptable. | ||
| 87 | |||
| 88 | ### Example Event | ||
| 89 | |||
| 90 | For example, this program could be displayed as an option to be clicked on any user profile. | ||
| 91 | |||
| 92 | ```yaml | ||
| 93 | { | ||
| 94 | "kind": 1227, | ||
| 95 | "content": "<base64-encoded WASM binary>", | ||
| 96 | "tags": | ||
| 97 | [ | ||
| 98 | ["name", "interactions"], | ||
| 99 | ["description", "dispĺays interactions between me and someone else"], | ||
| 100 | ["param", "me", "myself", "public_key", "required"], | ||
| 101 | [ | ||
| 102 | "param", | ||
| 103 | "public_key", | ||
| 104 | "person with whom I want to see my interactions", | ||
| 105 | "public_key", | ||
| 106 | "required" | ||
| 107 | ] | ||
| 108 | ] | ||
| 109 | } | ||
| 110 | ``` | ||
| 111 | |||
| 112 | ### Parameter Passing | ||
| 113 | |||
| 114 | 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 encoding: | ||
| 115 | |||
| 116 | | Type | Encoding | Size | | ||
| 117 | | ------------ | --------------------------------------------------------------------- | ------------- | | ||
| 118 | | `public_key` | 32 bytes (should all be set to zero if the parameter is not provided) | 32 bytes | | ||
| 119 | | `event` | i32 handle | 4 bytes | | ||
| 120 | | `string` | u32_be length followed by UTF-8 bytes | 4 + len bytes | | ||
| 121 | | `number` | i32 | 4 bytes | | ||
| 122 | | `timestamp` | unix timestamp as u32_be | 4 bytes | | ||
| 123 | | `relay` | relay URL, same as string | 4 + len bytes | | ||
| 124 | |||
| 125 | For example: in a program with parameters `[me: public_key, target_event: event, target_relay: relay]` the buffer layout would be: | ||
| 126 | |||
| 127 | ``` | ||
| 128 | [ 32-byte current user pubkey ][ 4-byte i32 handle ][ 4-byte len for relay URL ][ UTF-8 relay URL ] | ||
| 129 | ``` | ||
| 130 | |||
| 131 | The WASM program reads this by iterating through the declared parameters in order, reading the appropriate number of bytes for each type. | ||
| 132 | |||
| 133 | --- | ||
| 134 | |||
| 135 | ## Host Provided API | ||
| 136 | |||
| 137 | These are the functions the host must expose to be called by the WASM program. | ||
| 138 | |||
| 139 | 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. | ||
| 140 | |||
| 141 | ### `nostr.req_new() -> i32` | ||
| 142 | |||
| 143 | Create a new empty request handle. | ||
| 144 | |||
| 145 | ### `nostr.req_add_author(req: i32, pubkey_ptr: i32) -> void` | ||
| 146 | |||
| 147 | Add a pubkey to the `authors` filter. It must be a pointer to a 32-byte buffer. | ||
| 148 | |||
| 149 | ### `nostr.req_add_author_hex(req: i32, pubkey_hex_ptr: i32) -> void` | ||
| 150 | |||
| 151 | Same as `req_add_author` but with a hex-encoded pubkey string. Length is assumed to be 64 characters. | ||
| 152 | |||
| 153 | ### `nostr.req_add_id(req: i32, id_ptr: i32) -> void` | ||
| 154 | |||
| 155 | Add an id to the `ids` filter. It must be a pointer to a 32-byte buffer. | ||
| 156 | |||
| 157 | ### `nostr.req_add_id_hex(req: i32, id_hex_ptr: i32) -> void` | ||
| 158 | |||
| 159 | Same as `req_add_id` but with a hex-encoded id string. Length is assumed to be 64 characters. | ||
| 160 | |||
| 161 | ### `nostr.req_add_kind(req: i32, kind: i32) -> void` | ||
| 162 | |||
| 163 | Add a kind integer to the `kinds` filter. | ||
| 164 | |||
| 165 | ### `nostr.req_add_tag(req: i32, tag_ptr: i32, tag_len: i32, value_ptr: i32, value_len: i32) -> void` | ||
| 166 | |||
| 167 | Add a value to a tag filter. `tag_ptr/tag_len` points to the tag name (e.g. `"p"` -- 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. | ||
| 168 | |||
| 169 | ### `nostr.req_add_tag_bin32(req: i32, tag_ptr: i32, value_ptr: i32) -> void` | ||
| 170 | |||
| 171 | Same as `req_add_tag` but the value is a 32-byte binary buffer that the host will convert to hex. `value_ptr` must point to a 32-byte buffer. This is useful for `#p`, `#e`, and other tag filters that need pubkey or event ID values. | ||
| 172 | |||
| 173 | Hosts may deduplicate filter values (authors, ids, kinds, and tag values) when the module adds the same value more than once via `req_add_*`. | ||
| 174 | |||
| 175 | ### `nostr.req_set_limit(req: i32, limit: i32) -> void` | ||
| 176 | |||
| 177 | Sets the `"limit"` attribute of the filter. | ||
| 178 | |||
| 179 | ### `nostr.req_set_since(req: i32, timestamp: i32) -> void` | ||
| 180 | |||
| 181 | Sets the `"since"` attribute of the filter. | ||
| 182 | |||
| 183 | ### `nostr.req_set_until(req: i32, timestamp: i32) -> void` | ||
| 184 | |||
| 185 | Sets the `"until"` attribute of the filter. | ||
| 186 | |||
| 187 | ### `nostr.req_set_search(req: i32, ptr: i32, len: i32) -> void` | ||
| 188 | |||
| 189 | Sets the `"search"` attribute of the filter. | ||
| 190 | |||
| 191 | ### `nostr.req_add_relay(req: i32, ptr: i32, len: i32) -> void` | ||
| 192 | |||
| 193 | Explicitly target a relay URL for this request. | ||
| 194 | |||
| 195 | If no relays are added to a request, the client should apply the following relay selection heuristics: | ||
| 196 | |||
| 197 | - If `authors` are specified, use the NIP-65 **write** relays of those pubkeys. | ||
| 198 | - If `#p` or `#P` tag filters are specified, use the NIP-65 **read** relays of those pubkeys. | ||
| 199 | - Otherwise, use an arbitrary set of relays or just fail. | ||
| 200 | |||
| 201 | ### `nostr.req_close_on_eose(req: i32) -> void` | ||
| 202 | |||
| 203 | Mark this request to automatically close after `EOSE`. | ||
| 204 | |||
| 205 | ### `nostr.subscribe(req: i32) -> i32` | ||
| 206 | |||
| 207 | 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. | ||
| 208 | |||
| 209 | The globally exported `on_event` function will be called with the results. | ||
| 210 | |||
| 211 | - The module must call `nostr.drop(event)` on each non-zero event handle when done with it. | ||
| 212 | - If `nostr.req_close_on_eose` was set, the host drops the subscription handle after the EOSE callback. The module must not drop it. | ||
| 213 | |||
| 214 | To cancel a live subscription call `nostr.drop(sub)`. | ||
| 215 | |||
| 216 | ### `nostr.event_get_id(event_handle: i32) -> i32` | ||
| 217 | |||
| 218 | Returns a pointer to a 32-byte buffer containing the event ID. | ||
| 219 | |||
| 220 | ### `nostr.event_get_id_hex(event_handle: i32) -> i32` | ||
| 221 | |||
| 222 | Returns a pointer to a 64-character string containing the hex event ID. | ||
| 223 | |||
| 224 | ### `nostr.event_get_pubkey(event_handle: i32) -> i32` | ||
| 225 | |||
| 226 | Returns a pointer to a 32-byte buffer containing the public key of the event author. | ||
| 227 | |||
| 228 | ### `nostr.event_get_pubkey_hex(event_handle: i32) -> i32` | ||
| 229 | |||
| 230 | Returns a pointer to a 64-character string containing the hex public key of the event author. | ||
| 231 | |||
| 232 | ### `nostr.event_get_kind(event_handle: i32) -> i32` | ||
| 233 | |||
| 234 | Returns the kind integer directly. | ||
| 235 | |||
| 236 | ### `nostr.event_get_created_at(event_handle: i32) -> i32` | ||
| 237 | |||
| 238 | Returns the unix timestamp directly. | ||
| 239 | |||
| 240 | ### `nostr.event_get_content(event_handle: i32) -> i32` | ||
| 241 | |||
| 242 | Returns a pointer to a buffer containing the event content. | ||
| 243 | |||
| 244 | ### `nostr.event_get_tag_count(event_handle: i32) -> i32` | ||
| 245 | |||
| 246 | Returns the total number of tags on this event. | ||
| 247 | |||
| 248 | ### `nostr.event_get_tag_item_count(event_handle: i32, tag_index: i32) -> i32` | ||
| 249 | |||
| 250 | Returns the number of items in the tag at `tag_index`. | ||
| 251 | |||
| 252 | ### `nostr.event_get_tag_item(event_handle: i32, tag_index: i32, item_index: i32) -> i32` | ||
| 253 | |||
| 254 | Returns a pointer to a buffer containing the item at `(tag_index, item_index)`. | ||
| 255 | |||
| 256 | ### `nostr.event_get_tag_item_bin32(event_handle: i32, tag_index: i32, item_index: i32) -> i32` | ||
| 257 | |||
| 258 | Same as `event_get_tag_item`, but returns a 32-byte buffer of the item if it happened to be a pubkey or an event id; 0 otherwise. | ||
| 259 | |||
| 260 | ### `nostr.event_get_tag_item_by_name(event_handle: i32, name_ptr: i32, name_len: i32, item_index: i32) -> i32` | ||
| 261 | |||
| 262 | 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. | ||
| 263 | |||
| 264 | ### `nostr.event_get_tag_item_by_name_bin32(event_handle: i32, name_ptr: i32, name_len: i32, item_index: i32) -> i32` | ||
| 265 | |||
| 266 | Same as `event_get_tag_item_by_name`, but returns a 32-byte buffer of the value if it happened to be a pubkey or an event id; 0 otherwise. | ||
| 267 | |||
| 268 | ### `nostr.display(event: i32) -> void` | ||
| 269 | |||
| 270 | Render an event through the client's native note renderer. The event handle is not consumed. | ||
| 271 | |||
| 272 | ### `nostr.log(ptr: i32, len: i32) -> void` | ||
| 273 | |||
| 274 | Emit a log message to the host's debug console or developer tooling. The string at `ptr/len` is the message. | ||
| 275 | |||
| 276 | ### `nostr.drop(handle: i32) -> void` | ||
| 277 | |||
| 278 | Releases any handle: unconsumed request, active subscription (cancels it), individual event, or list (also releases all contained event handles). | ||
| 279 | |||
| 280 | --- | ||
| 281 | |||
| 282 | ## Example Programs and Host | ||
| 283 | |||
| 284 | - JavaScript host runtime example: https://viewsource.win/fiatjaf.com/nprogram/_/2e7d56d1536fcca8c49e7b2e99db8062f8df9ae1/~/src/nprogram-host.ts | ||
| 285 | - Rust example (basic, just reads from a relay): https://viewsource.win/fiatjaf.com/nprogram/_/2e7d56d1536fcca8c49e7b2e99db8062f8df9ae1/~/examples/basic/src/lib.rs | ||
| 286 | - Zig example (displays the history of all interactions between the current user and some other user): https://viewsource.win/fiatjaf.com/nprogram/_/2e7d56d1536fcca8c49e7b2e99db8062f8df9ae1/~/examples/interactions/main.zig | ||
| 287 | - TinyGo example (fetches all notifications, only display the unreplied ones): https://viewsource.win/fiatjaf.com/nprogram/_/2e7d56d1536fcca8c49e7b2e99db8062f8df9ae1/~/examples/unreplied/main.go | ||
| 288 | - AssemblyScript example (fetches events from multiple relays and does a diff of the content): https://viewsource.win/fiatjaf.com/nprogram/_/2e7d56d1536fcca8c49e7b2e99db8062f8df9ae1/~/examples/old-trending/main.ts | ||