feat: implement StreamCreated handler#43
Conversation
- Add StreamCreated event handler for parsing Soroban events - Map event payload to Stream record with event identity tracking - Add deterministic event identity for idempotency checks - Add mocked payload tests covering: - Payload parsing and validation - Event identity derivation - Record mapping - Handler idempotency - Multiple stream ID handling
|
@d3vobed Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits. You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀 |
📝 WalkthroughWalkthroughAdds StreamCreated event types, payload validation, identity generation, record mapping, handler composition, package exports, and Vitest coverage for parsing and handler behavior. ChangesStreamCreated handler
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 ESLint
ESLint install failed. For unrecoverable errors, disable the tool in CodeRabbit configuration. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@indexer/streams/src/handlers/streamCreated.ts`:
- Around line 22-31: The StreamCreated handler currently trusts a type cast
after JSON.parse, so invalid shapes can still produce a malformed StreamRecord.
Update the logic in streamCreated.ts, around the parsed payload handling in the
StreamCreatedPayload flow, to perform a real runtime validation of the parsed
object before building the return value. Use the existing streamCreated event
handler path to reject or guard against missing or wrong-typed streamId, sender,
recipient, amount, startTime, and endTime fields, and only return the record
once validation passes.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: cc30f585-3708-4e5f-b38f-f39951ab7f30
📒 Files selected for processing (4)
indexer/streams/src/handlers/streamCreated.test.tsindexer/streams/src/handlers/streamCreated.tsindexer/streams/src/handlers/types.tsindexer/streams/src/index.ts
| const parsed = JSON.parse(event.data) as StreamCreatedPayload; | ||
|
|
||
| return { | ||
| streamId: parsed.streamId, | ||
| sender: parsed.sender, | ||
| recipient: parsed.recipient, | ||
| amount: parsed.amount, | ||
| startTime: parsed.startTime, | ||
| endTime: parsed.endTime, | ||
| }; |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Validate the parsed payload at runtime before returning it.
Line 22 only proves that event.data is valid JSON. The as StreamCreatedPayload cast does not verify shape, so {} or a payload with non-string fields will flow through Lines 24-30 and produce a malformed StreamRecord. That breaks the handler’s insert contract and undermines the “payload validation” objective for this event.
Proposed fix
+function assertStreamCreatedPayload(value: unknown): asserts value is StreamCreatedPayload {
+ if (
+ typeof value !== "object" ||
+ value === null ||
+ typeof (value as Record<string, unknown>).streamId !== "string" ||
+ typeof (value as Record<string, unknown>).sender !== "string" ||
+ typeof (value as Record<string, unknown>).recipient !== "string" ||
+ typeof (value as Record<string, unknown>).amount !== "string" ||
+ typeof (value as Record<string, unknown>).startTime !== "string" ||
+ typeof (value as Record<string, unknown>).endTime !== "string"
+ ) {
+ throw new Error("Invalid StreamCreated payload");
+ }
+}
+
export function parseStreamCreatedPayload(event: StreamCreatedEvent): StreamCreatedPayload {
const eventName = event.topics[0];
if (!eventName || eventName !== STREAM_CREATED_TOPIC) {
throw new Error(
`Expected ${STREAM_CREATED_TOPIC} event topic, got ${eventName ?? "undefined"}`,
);
}
- const parsed = JSON.parse(event.data) as StreamCreatedPayload;
+ const parsed: unknown = JSON.parse(event.data);
+ assertStreamCreatedPayload(parsed);
return {
streamId: parsed.streamId,
sender: parsed.sender,
recipient: parsed.recipient,📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const parsed = JSON.parse(event.data) as StreamCreatedPayload; | |
| return { | |
| streamId: parsed.streamId, | |
| sender: parsed.sender, | |
| recipient: parsed.recipient, | |
| amount: parsed.amount, | |
| startTime: parsed.startTime, | |
| endTime: parsed.endTime, | |
| }; | |
| function assertStreamCreatedPayload(value: unknown): asserts value is StreamCreatedPayload { | |
| if ( | |
| typeof value !== "object" || | |
| value === null || | |
| typeof (value as Record<string, unknown>).streamId !== "string" || | |
| typeof (value as Record<string, unknown>).sender !== "string" || | |
| typeof (value as Record<string, unknown>).recipient !== "string" || | |
| typeof (value as Record<string, unknown>).amount !== "string" || | |
| typeof (value as Record<string, unknown>).startTime !== "string" || | |
| typeof (value as Record<string, unknown>).endTime !== "string" | |
| ) { | |
| throw new Error("Invalid StreamCreated payload"); | |
| } | |
| } | |
| export function parseStreamCreatedPayload(event: StreamCreatedEvent): StreamCreatedPayload { | |
| const eventName = event.topics[0]; | |
| if (!eventName || eventName !== STREAM_CREATED_TOPIC) { | |
| throw new Error( | |
| `Expected ${STREAM_CREATED_TOPIC} event topic, got ${eventName ?? "undefined"}`, | |
| ); | |
| } | |
| const parsed: unknown = JSON.parse(event.data); | |
| assertStreamCreatedPayload(parsed); | |
| return { | |
| streamId: parsed.streamId, | |
| sender: parsed.sender, | |
| recipient: parsed.recipient, | |
| amount: parsed.amount, | |
| startTime: parsed.startTime, | |
| endTime: parsed.endTime, | |
| }; | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@indexer/streams/src/handlers/streamCreated.ts` around lines 22 - 31, The
StreamCreated handler currently trusts a type cast after JSON.parse, so invalid
shapes can still produce a malformed StreamRecord. Update the logic in
streamCreated.ts, around the parsed payload handling in the StreamCreatedPayload
flow, to perform a real runtime validation of the parsed object before building
the return value. Use the existing streamCreated event handler path to reject or
guard against missing or wrong-typed streamId, sender, recipient, amount,
startTime, and endTime fields, and only return the record once validation
passes.
Summary
Implements the
StreamCreatedevent handler for the payment streams domain as specified in #34.Changes
parseStreamCreatedPayloadvalidates the event topic matchesStreamCreatedand extracts the structured payload (streamId,sender,recipient,amount,startTime,endTime)mapStreamCreatedToRecordcombines the parsed payload with event identity metadata into aStreamRecordgetEventIdentityproduces a deterministic string fromcontractId:ledger:txHash:eventIndexfor idempotency checkshandleStreamCreatedcomposes the above into a single call, returning both the stream record and identityTesting
Verification
bun run indexer:type-check✅bun run indexer:test✅ (10 tests)bun run indexer:lint✅Closes #34
Summary by CodeRabbit
New Features
Bug Fixes
Tests