Skip to content

Commit 92a59b8

Browse files
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.
1 parent db3ef32 commit 92a59b8

7 files changed

Lines changed: 593 additions & 9 deletions

File tree

docs/architecture.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,10 +92,12 @@ All of the server's input and output artifacts live in one directory, the *io di
9292
| `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}`. |
9393
| `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. |
9494
| `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. |
9596
| `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. |
9697
| `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. |
9799

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.
99101

100102
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.
101103

@@ -177,6 +179,7 @@ sequenceDiagram
177179

178180
- `resultXdr` / `resultMetaXdr` in `getTransaction` responses (contract return values)
179181
- `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)
181184
- `ExtendFootprintTTL` and `RestoreFootprint` operations
182185
- `SCVec` / `SCMap` contract-argument types in the request encoder (`scval_to_json`)

docs/node-semantics.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ The semantics communicate with the Python process through files in the working d
1717
| `metadata.json` | K ↔ K | `{"latest_ledger": N}` — the ledger counter |
1818
| `receipts/receipt_<hash>.json` | K → Python | one stored receipt per transaction, keyed by tx hash |
1919
| `traces/trace_<hash>.jsonl` | K → Python | one execution trace per transaction (per-instruction records), keyed by tx hash |
20+
| `events_staged.jsonl` | K → Python | the contract events of the transaction in flight, one JSON record per `contract_event` call |
21+
| `events/events_<ledger>.json` | Python → K | one finished JSON array of Event objects per ledger, served by `getEvents` |
2022

2123
---
2224

@@ -39,7 +41,7 @@ insert-handleRequestFile → handleRequestFile
3941
4042
#dispatchMethod(method, request) ← routes on the "method" field
4143
42-
├─ getHealth / getNetwork / getLatestLedger / getTransaction / traceTransaction → #respond(...)
44+
├─ getHealth / getNetwork / getLatestLedger / getTransaction / traceTransaction / getEvents → #respond(...)
4345
4446
└─ sendTransaction → #runTx → run steps
4547
→ #finalizeTx → record receipt + bump ledger → #respond(...)
@@ -62,8 +64,9 @@ If `request.json` is absent, `insert-handleRequestFile` does not fire and K halt
6264
- `getNetwork``{ "friendbotUrl": null, "passphrase": ..., "protocolVersion": ... }` (passphrase/version come from the request, keeping the semantics network-agnostic)
6365
- `getLatestLedger` → reads `metadata.json` and returns `{ "id": <64 zeros>, "protocolVersion": ..., "sequence": <latest_ledger> }`
6466
- `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`).
6568

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}}`.
6770

6871
---
6972

docs/notes.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ State lives in the io dir as `state.kore` (KORE world state) and `metadata.json`
2323
## Tests (`src/tests/integration/`)
2424

2525
- `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.
2627
- `test_integration.py` and `test_unit.py` hold small sanity checks.
2728

2829
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
3536

3637
- `resultXdr` / `resultMetaXdr` are empty stubs (contract return values not surfaced).
3738
- `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.

docs/server.md

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ class StellarRpcServer:
1515
receipts_dir: Path # io_dir / 'receipts' — receipt_<hash>.json per transaction
1616
traces_dir: Path # io_dir / 'traces' — trace_<hash>.jsonl per transaction
1717
requests_dir: Path # io_dir / 'requests' — request_<n>.json archive
18+
events_dir: Path # io_dir / 'events' — events_<ledger>.json per ledger
1819
```
1920

2021
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'))
2829
server.handle_rpc('sendTransaction', {'transaction': xdr})
2930
```
3031

31-
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.
3233

3334
---
3435

@@ -51,7 +52,7 @@ At construction the server prepares the *io dir*, where `state.kore` lives at `i
5152
- **`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}`.
5253
- **`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.
5354

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.
5556

5657
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.)
5758

@@ -143,6 +144,30 @@ All methods are answered by the K semantics and follow the [Stellar RPC specific
143144

144145
`resultXdr` and `resultMetaXdr` are currently empty stubs. The receipt carries no trace — use `traceTransaction` with the same hash to fetch it.
145146

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`.
152+
153+
**Response**:
154+
```json
155+
{
156+
"latestLedger": 4,
157+
"events": [
158+
{
159+
"type": "contract", "ledger": 4, "ledgerClosedAt": "2024-05-18T04:00:00Z",
160+
"contractId": "C...", "id": "0000000017179873280-0000000000",
161+
"inSuccessfulContractCall": true, "txHash": "<64-char hex>",
162+
"topic": ["<base64 SCVal XDR>"], "value": "<base64 SCVal XDR>"
163+
}
164+
],
165+
"cursor": "0000000021474836480-0000000000"
166+
}
167+
```
168+
169+
`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.
170+
146171
---
147172

148173
## Failure fallback

0 commit comments

Comments
 (0)