feat(heph): server-side credit transfer support#161
Conversation
foxpatch-aleph
left a comment
There was a problem hiding this comment.
Well-structured implementation of server-side credit transfer support. The key design strengths are: (1) atomic post-insert + credit-apply in a single SQL transaction, preventing partial state; (2) proper validation order (deserialize → schema validate → self-transfer reject → balance check → apply); (3) parameterized SQL throughout (no injection); (4) the new credit_transfer module cleanly decouples the transfer schema from the alloy-dependent credit module; (5) solid test coverage with 8 handler unit tests and 3 end-to-end tests covering success, insufficient-balance rollback, and self-transfer rejection. The schema extension with nullable message_hash/counterparty/expiration_at is backward-compatible with existing distribution entries. No bugs or security issues found.
crates/heph/src/handlers/credit_transfer.rs (line 58): The comment about RFC3339 with Z suffix vs +00:00 is useful context — good callout for cross-ecosystem consistency with pyaleph.
crates/heph/src/handlers/post.rs (line 126): The unchecked_transaction choice is called out and justified in the comment. The commit() correctly propagates errors via map_err to ProcessingError::InternalError, ensuring the ? operator on the closure return value surfaces failures to process_message.
foxpatch-aleph
left a comment
There was a problem hiding this comment.
The credit transfer handler is well-structured with clean separation between deserialization, validation, and apply phases. The SDK module refactor and atomic transaction wrapping are solid. However, there's a correctness bug: when a credit transfer fails mid-dispatch (e.g., insufficient balance), the message is already committed to the messages table with status processed because step 6 runs before step 7 (type-specific dispatch). On retry, the duplicate check silently skips the message, so failed credit transfers are permanently stuck as processed despite no post being created.
crates/heph/src/handlers/mod.rs (line 395): Messages table commit before type-specific dispatch creates inconsistent state for failed credit transfers. When process_post rolls back a credit transfer (e.g. insufficient balance), the messages table already has the item_hash committed with status processed. On retry, step 1 sees status processed and silently skips (line 304: "processed" => return Ok(())). The message is permanently stuck as processed despite the post never being created. Consider either: (a) inserting the message row inside the same transaction as the post/credit-apply, or (b) defaulting credit-transfer messages to pending status and only promoting to processed after the apply succeeds, or (c) setting status to rejected when dispatch fails.
crates/heph/src/handlers/credit_transfer.rs (line 52): Consider adding a test for multi-recipient transfers. The current test suite only covers single-recipient cases. A test with 2+ recipients would verify that the sender balance decrement works correctly across loop iterations (same sender row, multiple updates).
crates/heph/tests/integration.rs (line 957): Minor: body.contains("\"code\":6") is sensitive to JSON serialization format. If the JSON serializer adds spacing (e.g. "code": 6), this would fail. Consider parsing the JSON response or using a less fragile match.
Summary
aleph_credit_transferPOST messages — heph now validates, records, and applies transfers emitted byaleph credit transfer(CLI side merged in feat(cli): add 'aleph credit transfer' subcommand #158).crates/heph/src/handlers/credit_transfer.rswithprocess_in_tx(tx, sender, item_hash, raw_content)doing deserialize → schema validate → self-transfer reject → balance check → atomic apply (recipient + sendercredit_historyrows, recipient upsert, sender decrement).handlers/post.rsdispatches credit-transfer posts inside a single SQL transaction so the post insert and the apply share atomicity (rollback on any error).credit_history: nullablemessage_hash,counterparty,expiration_atcolumns + index onmessage_hash. No migration (greenfield DB in scope).CreditTransferContentetc.) out of thecredits-feature-gatedaleph_sdk::creditmodule into a new always-compiledaleph_sdk::credit_transfermodule so heph doesn't pull alloy as a transitive dep.Out of scope (deferred):
aleph_credit_distribution/aleph_credit_expensehandlers, FIFO consumption with expiration carry-over, whitelist gating, balance recompute from history, expiration enforcement (column is advisory metadata only).Test plan
cargo test -p aleph-sdk credit_transfer::— 8 schema/validation unit tests passcargo test -p heph credit_transfer— 8 handler unit tests + 3 end-to-end tests throughprocess_messagepass (success, insufficient-balance rollback verified by zero post + zero history rows, self-transfer rejection)just test— full workspace, 689+ tests, 0 failuresjust check-typing— clippy-D warningscleangit diff --stat origin/main..HEADtouches only the 10 files in the design (4 SDK/CLI for the schema move + 6 in heph)🤖 Generated with Claude Code