Summary
record_tip (src/controllers/tip_controller.rs) persists tips as self-reported client data. Nothing verifies that a corresponding payment actually settled on the Stellar network before the tip enters the database, mutates leaderboards, triggers webhooks, and feeds analytics. Any client can fabricate tips of arbitrary amounts. For a tipping platform this is the core integrity gap.
Task
Build a verification pipeline that makes on-chain settlement the source of truth:
- Require a
tx_hash on tip submission. Before persisting, verify against Horizon/Soroban RPC (endpoint from typed config) that:
- the transaction exists and succeeded
- it contains a payment/
tip invocation whose amount, asset, destination (creator address), and memo match the submitted tip within exact equality (amounts in stroops — no float comparison anywhere)
- the transaction's source account matches the claimed tipper
- Enforce exactly-once ingestion: a
UNIQUE constraint on tx_hash (new migration) plus a race-safe insert (ON CONFLICT DO NOTHING + explicit duplicate error mapping) so concurrent submissions of the same hash cannot double-record.
- Two-phase state: tips enter as
pending_verification and transition to confirmed/rejected; webhooks, leaderboard updates, and stats must only fire on confirmed (audit and gate all existing side-effect paths, including the double-webhook-prone creator/tip controller interplay).
- Resilience: Horizon outages must not lose submissions — enqueue verification with bounded retries and backoff (reuse
src/queue/), a reconciliation job (src/jobs/) that re-drives stuck pending tips, and metrics for verification latency and failure classes.
- A verification bypass for tests only, implemented as an injected trait object (
TipVerifier) with a mock — never an env flag reachable in production builds.
- Tests: happy path against a mocked Horizon response fixture, amount/destination/memo mismatch rejections, duplicate
tx_hash race (two concurrent submissions), Horizon 5xx → retry → eventual confirm, and reconciliation of stuck tips.
Acceptance criteria
PR requirements (mandatory)
- ✅ Your PR must pass all checks — PRs with failing or skipped checks will not be merged.
- 📸 You must attach a screenshot in the PR description demonstrating the result (for this issue: a real testnet transaction being verified end-to-end, or the full test-suite output covering the verification pipeline).
- 🔗 You must link this issue number in your PR description (e.g.
Closes #<issue-number>). PRs without a linked issue will not be reviewed.
Summary
record_tip(src/controllers/tip_controller.rs) persists tips as self-reported client data. Nothing verifies that a corresponding payment actually settled on the Stellar network before the tip enters the database, mutates leaderboards, triggers webhooks, and feeds analytics. Any client can fabricate tips of arbitrary amounts. For a tipping platform this is the core integrity gap.Task
Build a verification pipeline that makes on-chain settlement the source of truth:
tx_hashon tip submission. Before persisting, verify against Horizon/Soroban RPC (endpoint from typed config) that:tipinvocation whose amount, asset, destination (creator address), and memo match the submitted tip within exact equality (amounts in stroops — no float comparison anywhere)UNIQUEconstraint ontx_hash(new migration) plus a race-safe insert (ON CONFLICT DO NOTHING+ explicit duplicate error mapping) so concurrent submissions of the same hash cannot double-record.pending_verificationand transition toconfirmed/rejected; webhooks, leaderboard updates, and stats must only fire onconfirmed(audit and gate all existing side-effect paths, including the double-webhook-prone creator/tip controller interplay).src/queue/), a reconciliation job (src/jobs/) that re-drives stuckpendingtips, and metrics for verification latency and failure classes.TipVerifier) with a mock — never an env flag reachable in production builds.tx_hashrace (two concurrent submissions), Horizon 5xx → retry → eventual confirm, and reconciliation of stuck tips.Acceptance criteria
confirmedstateconfirmedPR requirements (mandatory)
Closes #<issue-number>). PRs without a linked issue will not be reviewed.