feat(payments): bind zkEmail receipt commitments to payments (#119) - #132
Conversation
…ne-Labs#119) Payments can now carry an optional zkEmail receipt commitment — a salted hash of the buyer's email (e.g. H(email || ticket_id)) computed off-chain. The commitment binds off-chain delivery eligibility to the on-chain payment record while never exposing the raw email. - PaymentRecord gains zk_email_commitment: Option<BytesN<32>> (stored, never emitted). None = fully anonymous attendee. - New pay_for_ticket_with_commitment entrypoint accepts a commitment at payment time; existing pay_for_ticket / pay_for_ticket_with_options are unchanged (delegate with None) so no cross-contract caller breaks. - New bind_email_commitment(payer, payment_id, commitment): payer-owned, write-once binding for the ticket_id-salted case (ticket_id is only known after payment). Rejected once set and after refund. - New get_payment_commitment getter and verify_email_commitment view let an off-chain relayer prove delivery eligibility without revealing the email. - ReceiptCommitmentBound event carries only payment/event ids + timestamp; the commitment hash is never published. 10 new tests (96 total in payments) incl. a positive-control check proving the commitment never appears in any emitted event. Co-authored-by: Cursor <cursoragent@cursor.com>
|
@josephchimebuka hi pls resolve the merge conflict |
Resolve PaymentError enum conflict by keeping postponement errors (33–34) and assigning zkEmail commitment errors to 35–36. Co-authored-by: Cursor <cursoragent@cursor.com>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
🚧 Files skipped from review as they are similar to previous changes (2)
📝 WalkthroughWalkthroughAdds optional zkEmail receipt commitment support to the payments contract. Payment records can now store a zkEmail Receipt Commitment
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Suggested reviewers
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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 `@contracts/payments/src/lib.rs`:
- Around line 1617-1620: The get_payment_commitment API is collapsing storage
errors and missing payments into None via .ok().and_then(...), which hides
PaymentNotFound and future decode failures. Update get_payment_commitment in
contracts/payments/src/lib.rs to return Result<Option<BytesN<32>>, PaymentError>
and propagate storage::get_payment from storage::get_payment instead of
discarding the error, while preserving None only for the real “payer opted out”
case.
🪄 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 Plus
Run ID: 2673fb73-1e36-4900-92d2-ee0824776c1d
📒 Files selected for processing (5)
contracts/payments/src/errors.rscontracts/payments/src/events.rscontracts/payments/src/lib.rscontracts/payments/src/receipt_commitment_test.rscontracts/payments/src/types.rs
Return Result<Option<BytesN<32>>, PaymentError> so relayers can distinguish a missing payment from a payer who opted out of a commitment. Co-authored-by: Cursor <cursoragent@cursor.com>
Resolve conflicts with postponement, anon-claims, and merged PR BuidlZone-Labs#132 (zkEmail commitments). Keep both revenue-split and commitment APIs with non-overlapping PaymentError codes (35–42). Co-authored-by: Cursor <cursoragent@cursor.com>
Linked issue
Closes #119
What this PR does
The payments contract can now bind an optional zkEmail receipt commitment to each payment record. A buyer (or relayer on their behalf) supplies a salted hash of their email — e.g.
H(email || ticket_id)— computed entirely off-chain. The hash is stored on the payment record so an off-chain relayer can verify delivery eligibility without ever putting the raw email on-chain. The field is optional: payments without a commitment proceed normally for fully anonymous attendees.Two integration paths are supported:
pay_for_ticket_with_commitmententrypoint (when the commitment is already known).bind_email_commitment(when the commitment must be salted withticket_id, which is only assigned once the payment is created).Existing
pay_for_ticketandpay_for_ticket_with_optionsentrypoints are unchanged and delegate withNone, so no cross-contract callers break.Change type
Storage impact
PaymentRecord.zk_email_commitmentOption<BytesN<32>>None= anonymous / opted outIs this a breaking storage change?
On-chain vs. off-chain behaviour
PaymentRecord.zk_email_commitmentReceiptCommitmentBoundemits only ids + timestamp; positive-control test proves commitment hex never appears in any eventNonecommitment; existing entrypoints unchangedget_payment_commitment+verify_email_commitmentCross-contract impact
<!-- fn name -->in<!-- contract -->Existing cross-contract callers (
eventcontract →pay_for_ticket) continue to work unchanged because the new field defaults toNoneinsidecreate_payment.Privacy checklist
Note: the legacy
email_hashfield onPaymentReceiptRequestedstill emits its hash (pre-existing behaviour). The newzk_email_commitmentis deliberately not emitted.Security checklist
require_auth/ admin check)bind_email_commitmentrequires payer auth, is write-once, and is rejected after refund.Test coverage
New tests added:
test_pay_with_commitment_stores_and_reads_backtest_commitment_is_optionalNonecommitmenttest_bind_commitment_after_paymenttest_bind_commitment_is_write_onceCommitmentAlreadySettest_bind_commitment_requires_payer_ownershiptest_bind_commitment_rejected_after_refundtest_verify_email_commitment_matches_and_mismatchestest_verify_returns_false_when_no_commitmenttest_commitment_is_stored_but_never_emittedtest_bind_event_does_not_leak_commitmentReceiptCommitmentBoundevent carries no hashEdge cases covered:
Test count: 10 new, 96 total in payments-contract; 74 event-contract tests still pass.
Acceptance criteria sign-off
zk_email_commitment: Option<BytesN<32>>per payment recordPaymentRecord.zk_email_commitmentintypes.rs;test_pay_with_commitment_stores_and_reads_backpay_for_ticket_with_commitment+bind_email_commitment;test_bind_commitment_after_paymentBytesN<32>accepted;test_commitment_is_stored_but_never_emitted,test_bind_event_does_not_leak_commitmentget_payment_commitment,verify_email_commitment;test_verify_email_commitment_matches_and_mismatchesNone;test_commitment_is_optionalWhat this PR deliberately does NOT cover
pay_for_ticket_with_commitment(callers can adopt incrementally)email_hash/PaymentReceiptRequestedemission pathReviewer focus areas
zk_email_commitmentis never emitted — only stored (see privacy tests with positive control against legacyemail_hashemission).bind_email_commitment(payer auth, refund guard).pay_for_ticketcallers remain unaffected (additive optional field only).Checklist
cargo fmtandcargo clippyTest plan
cargo fmt -p payments-contractcargo clippy -p payments-contract --all-targets -- -D warningscargo test -p payments-contract -p event-contract --offlinecargo build -p payments-contract --release --offlineMade with Cursor
Summary by CodeRabbit