You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat: implement getEvents with contract event capture
The upstream soroban semantics implement the contract_event host
function as a no-op, so node.md now shadows it: the interception rule
resolves the topics vector and data value from the host objects and
stages one JSON record per event in events_staged.jsonl, alongside how
the tracer appends trace records. After a successful sendTransaction
the server converts the staged records into one finished
events/events_<ledger>.json per ledger (base64 SCVal XDR, strkey
contract ids, and TOID-style event ids are XDR work K cannot do).
getEvents itself is dispatched in K: it validates the requested window
against the chain tip, scans the per-ledger event files, applies the
spec's filters (type, contractIds, topics with * and ** wildcards), and
paginates with TOID-style cursors. Structural parameter validation
(filter and segment counts, cursor format, xdrFormat - 'json' is
rejected as unsupported) lives in the server next to the existing param
checks. System events are never emitted, and events whose values have
no staging representation (maps, errors, 256-bit ints) are dropped with
a warning instead of fabricating XDR.
Covered by the getEvents integration tests; the full test_server.py
suite still passes.
Copy file name to clipboardExpand all lines: docs/architecture.md
+5-2Lines changed: 5 additions & 2 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -92,10 +92,12 @@ All of the server's input and output artifacts live in one directory, the *io di
92
92
|`receipts/receipt_<hash>.json`| persistent | the semantics (on success) or the server (on failure) | one stored receipt per transaction, keyed by tx hash, answering `getTransaction`. Each is `{status, ledger, createdAt, envelopeXdr, resultXdr, resultMetaXdr}`. |
93
93
|`traces/trace_<hash>.jsonl`| persistent | the semantics | one execution trace per transaction, keyed by tx hash — the instruction-level records, one JSON object per line. `traceTransaction` returns this file's contents. |
94
94
|`requests/request_<n>.json`| persistent | the server | an archive of each incoming JSON-RPC request, numbered by a monotonic counter, kept for debugging. |
95
+
|`events/events_<ledger>.json`| persistent | the server | one JSON array per ledger with the contract events emitted in it, already in the spec's Event shape. Built by the server from the staged records after a successful transaction; read back by the K `getEvents` rules. |
95
96
|`request.json`| transient | the server | the request envelope for the call in flight (`method`, `id`, `now`, and method-specific fields). The semantics remove it once they respond. |
96
97
|`response.json`| transient | the semantics | the JSON-RPC response (`{jsonrpc, id, result}`) for the most recent call. The server reads it back; it is absent when a transaction gets stuck. |
98
+
|`events_staged.jsonl`| transient | the K semantics | the contract events of the transaction in flight, one JSON record per `contract_event` call. The server converts them into `events/events_<ledger>.json` after a successful run and removes the file. |
97
99
98
-
Receipts, traces, and request archives are split into one file per item — keyed by tx hash, or numbered — so that no single file grows without bound as the chain advances. The server creates the `receipts/`, `traces/`, and `requests/` directories before the semantics run, because the K file-system hooks open files with POSIX `open()`, which does not create parent directories.
100
+
Receipts, traces, events, and request archives are split into one file per item — keyed by tx hash or ledger, or numbered — so that no single file grows without bound as the chain advances. The server creates the `receipts/`, `traces/`, `requests/`, and `events/` directories before the semantics run, because the K file-system hooks open files with POSIX `open()`, which does not create parent directories.
99
101
100
102
The world state stays in KORE (rather than a JSON snapshot) because an uploaded wasm module is a `ModuleDecl` that the semantics cannot reconstruct from bytes — only `wasm2kast` (Python) can produce it. The receipts and the ledger counter, by contrast, are plain data and live in JSON files, which the semantics read and write directly via the file-system hooks.
101
103
@@ -177,6 +179,7 @@ sequenceDiagram
177
179
178
180
-`resultXdr` / `resultMetaXdr` in `getTransaction` responses (contract return values)
179
181
-`simulateTransaction` (dry-run without state mutation)
180
-
-`getEvents`, `getLedgerEntries`, `getFeeStats` and other read-only RPC methods
182
+
-`getLedgerEntries`, `getFeeStats` and other read-only RPC methods
183
+
-`system` events in `getEvents` (the semantics have no source of them)
181
184
-`ExtendFootprintTTL` and `RestoreFootprint` operations
182
185
-`SCVec` / `SCMap` contract-argument types in the request encoder (`scval_to_json`)
→ #finalizeTx → record receipt + bump ledger → #respond(...)
@@ -62,8 +64,9 @@ If `request.json` is absent, `insert-handleRequestFile` does not fire and K halt
62
64
-`getNetwork` → `{ "friendbotUrl": null, "passphrase": ..., "protocolVersion": ... }` (passphrase/version come from the request, keeping the semantics network-agnostic)
-`getTransaction` → reads the hash's `receipts/receipt_<hash>.json` file; returns the stored receipt merged with the current `latestLedger`/`latestLedgerCloseTime`, or `{ "status": "NOT_FOUND", ... }` when the file is absent
67
+
-`getEvents` → scans the `events/events_<ledger>.json` files of the requested window, applies the request's filters (type, contract ids, topic matchers with `*`/`**`), and paginates; returns `{ "latestLedger": ..., "events": [...], "cursor": ... }`. The events themselves were captured during `sendTransaction`: a rule in `node.md` shadows the upstream no-op `contract_event` host function, resolves the topics/data host objects, and stages them in `events_staged.jsonl`; the Python server then produces the finished per-ledger files (base64 SCVal XDR and strkey ids are XDR work K cannot do). A `startLedger` beyond the chain tip is answered with `#respondError` (JSON-RPC `-32600`).
65
68
66
-
`#respond(ID, RESULT)` is the shared terminal: it writes the JSON-RPC envelope to `response.json`, removes `request.json`, and sets the exit code to 0.
69
+
`#respond(ID, RESULT)` is the shared terminal: it writes the JSON-RPC envelope to `response.json`, removes `request.json`, and sets the exit code to 0.`#respondError(ID, CODE, MESSAGE)` is its error-shaped counterpart, writing `{jsonrpc, id, error: {code, message}}`.
Copy file name to clipboardExpand all lines: docs/notes.md
+3-1Lines changed: 3 additions & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -23,6 +23,7 @@ State lives in the io dir as `state.kore` (KORE world state) and `metadata.json`
23
23
## Tests (`src/tests/integration/`)
24
24
25
25
-`test_server.py` drives the running HTTP server end-to-end. It exercises the read-only methods, `sendTransaction` + `getTransaction`, ledger increments, the full lifecycle (create → upload wasm → deploy → invoke), and the `traceTransaction` flows. `test_call_tx_with_args` deploys `args.wat` and calls functions with `bool`, `u32`, `i32`, `u64`, `i64`, `u128`, `i128`, and `symbol` arguments, exercising the `scval_to_json` / `#decodeArg` pipeline.
26
+
-`test_get_events.py` covers `getEvents`: request validation, the empty-window shape, the full shape of an event emitted via `contract_event` (`data/wasm/events.wat`), filtering, and pagination.
26
27
-`test_integration.py` and `test_unit.py` hold small sanity checks.
27
28
28
29
Run with `make test` (requires `make kdist-build` first).
@@ -35,4 +36,5 @@ The tests do not yet cover `bytes` / `address` SCVal arguments or `SCVec` / `SCM
35
36
36
37
-`resultXdr` / `resultMetaXdr` are empty stubs (contract return values not surfaced).
37
38
-`SCVec` / `SCMap` contract arguments are not yet encoded.
38
-
-`simulateTransaction`, `getEvents`, `getLedgerEntries`, `getFeeStats`, and TTL/footprint operations are not implemented.
39
+
-`simulateTransaction`, `getLedgerEntries`, `getFeeStats`, and TTL/footprint operations are not implemented.
40
+
-`getEvents` serves contract events only: `system` events are never emitted (the semantics have no source of them), and events whose topics/data have no staging representation (maps, errors, 256-bit ints) are dropped with a warning. `xdrFormat: "json"` is rejected.
events_dir: Path # io_dir / 'events' — events_<ledger>.json per ledger
18
19
```
19
20
20
21
The server is a plain `http.server.HTTPServer` (not pyk's `JsonRpcServer`). A `BaseHTTPRequestHandler` reads each POST body and calls `_handle`, which parses the JSON-RPC frame and delegates to `handle_rpc`.
@@ -28,7 +29,7 @@ server = StellarRpcServer(io_dir=Path('out'))
For `sendTransaction` it builds the request envelope with `encoder.build_tx_request` and runs it with `interpreter.run`; for the read-only methods (`getHealth`, `getNetwork`, `getLatestLedger`, `getTransaction`, `traceTransaction`) it builds a small envelope and runs it. In every case the *content* of the response is produced by the semantics (`node.md`), not by Python — the one exception is the failure fallback (below). Each call is logged to stderr.
32
+
For `sendTransaction` it builds the request envelope with `encoder.build_tx_request` and runs it with `interpreter.run`; for the read-only methods (`getHealth`, `getNetwork`, `getLatestLedger`, `getTransaction`, `traceTransaction`, `getEvents`) it builds a small envelope and runs it. In every case the *content* of the response is produced by the semantics (`node.md`), not by Python — the one exception is the failure fallback (below). Each call is logged to stderr.
32
33
33
34
---
34
35
@@ -51,7 +52,7 @@ At construction the server prepares the *io dir*, where `state.kore` lives at `i
51
52
-**`state.kore` absent** — `interpreter.empty_config()` produces the initial idle K configuration (a blank-slate state with no accounts, contracts, or storage) and writes it; `metadata.json` is seeded with `{"latest_ledger": 0}`.
52
53
-**`state.kore` present** — it is used as-is, and `metadata.json` is seeded only if missing. This lets you resume a previous session (ledger counter and stored receipts included) or start against a pre-built state.
53
54
54
-
In both cases the server creates the `receipts/`, `traces/`, and `requests/` directories if they do not already exist, because the K file-system hooks write into them but cannot create them.
55
+
In both cases the server creates the `receipts/`, `traces/`, `requests/`, and `events/` directories if they do not already exist, because the K file-system hooks write into them but cannot create them.
55
56
56
57
Once the socket is bound, `serve` logs three lines to stderr: whether it is starting from a fresh state (an empty io-dir) or resuming an existing one (with the latest ledger), the io-dir path, and the listening address. Instruction tracing is always on, so every transaction the semantics run produces a trace. (Tracing only produces records for contract invocations.)
57
58
@@ -143,6 +144,30 @@ All methods are answered by the K semantics and follow the [Stellar RPC specific
143
144
144
145
`resultXdr` and `resultMetaXdr` are currently empty stubs. The receipt carries no trace — use `traceTransaction` with the same hash to fetch it.
145
146
147
+
### `getEvents`
148
+
149
+
`getEvents` returns the contract events emitted in a ledger window — `startLedger` (inclusive) to `endLedger` (exclusive) — optionally narrowed by up to five filters (`type`, `contractIds`, `topics` with `*`/`**` wildcards) and paginated with `pagination.cursor` / `pagination.limit` (default 100). A cursor replaces `startLedger`/`endLedger` and resumes strictly after the last returned event.
150
+
151
+
Events are captured during `sendTransaction`: the semantics intercept the `contract_event` host function and stage each event's topics and data, and after a successful run the server converts the staged records into one finished `events/events_<ledger>.json` per ledger (`_finalize_events` — the base64 SCVal XDR, strkey, and TOID encodings are XDR work K cannot do). The K `getEvents` rules scan, filter, and paginate those files. Parameter validation lives in `_get_events_envelope`; `xdrFormat: "json"` is not supported and is rejected with `-32602`.
`system` events are never emitted (the semantics have no source of them), and events carrying values with no staging representation (maps, errors, 256-bit ints) are dropped with a warning rather than served with fabricated XDR.
0 commit comments