feat: add soulbound donation receipts (#146) - #158
Conversation
4435b1c to
742627b
Compare
|
Soulbound donation receipts are a brilliant UX touch. CI green, squashing in. ✅ |
|
After landing the recent batch, this branch now has merge conflicts with |
742627b to
8a5ea98
Compare
|
Rebased onto latest main and pushed @ibrahimmosouf-png — the conflict was a one-line import overlap with the #118 milestone-layout change. Contract clippy |
|
@neromtoobad please remove test snapshots |
8a5ea98 to
adba446
Compare
57f9e31 to
3dc6436
Compare
|
Rebased onto latest main @ibrahimmosouf-png. The conflict was an error-code + event collision with the circuit breaker (#183, now on main): both PRs claimed codes 90–92. I kept #183's |
ibrahimmosouf-png
left a comment
There was a problem hiding this comment.
please we still have test snapshots, remove them.
3dc6436 to
94c7c7c
Compare
|
Done @ibrahimmosouf-png — apologies, the big rebase across #185 swept the regenerated |
94c7c7c to
983641d
Compare
Rebuilt cleanly on current main (post OrbitChainLabs#180/OrbitChainLabs#186-OrbitChainLabs#190/OrbitChainLabs#163): receipt module + entrypoints on the decomposed crate, receipt errors at 91/92/93 (90 is the merged circuit breaker's AssetBlocked), ReceiptData/ReceiptCount keys appended at the DataKey tail after CachedReport. Once a campaign is finalised — goal reached and final milestone released — each donor can claim a permanent, non-transferable receipt attesting to their cumulative contribution: claim_receipt, is_finalised, get_receipt, receipt_balance, receipt_total_supply, and explicit soulbound guards (transfer/transfer_from/approve panic with ReceiptNonTransferable). receipt_minted event mirrors donation_received's topic shape for per-campaign indexer subscriptions. 13 receipt tests green; full campaign suite passes. Closes OrbitChainLabs#146
983641d to
54665a4
Compare
|
Properly fixed now @ibrahimmosouf-png — my earlier pushes were checking the wrong diff and snapshot artifacts kept surviving in the PR. I rebuilt the branch from scratch on current main: the PR diff is now exactly 6 source files (receipt.rs + tests, and the types/storage/event/lib wiring) with zero |
Summary
Closes #146. Once a campaign is finalised — goal reached and final milestone released — each donor can claim a permanent, non-transferable receipt recording what they contributed.
Three deviations from the issue text — and why
These were raised on the issue before I started; documented in the module header too. Happy to rework any of them.
1. Claim (pull), not auto-mint (push). The issue says "mint soulbound SAC tokens to donors" at finalise. That isn't implementable here: donors are stored under
DataKey::DonorData(donor)with no list or index, and Soroban has no key enumeration — the contract cannot walk its donors. Even given a list, minting to every donor inside one transaction is an unbounded loop that would exceed resource limits on a large campaign. So each donor claims their own receipt: O(1), no enumeration, no gas cliff.2. A soulbound record, not a SAC. A Stellar Asset Contract implements the standard token interface —
transferis built in and cannot be made to panic, so the acceptance criterion "soulbound enforcement (transfer panics)" is unreachable with a real SAC. The receipt is instead a donor-keyed record with token-shaped reads (balance,total_supply) andtransfer/transfer_from/approvepanicking withReceiptNonTransferable. The guards are exposed deliberately rather than omitted: a missing function merely makes transfers impossible, whereas an explicit panic makes the intent legible on-chain and returns a precise error.3.
finaliseis a condition, not an entrypoint. There's nofinalisefunction in the contract, sois_finalised()computes it:raised_amount >= goal_amountAND final milestone== Released. Both halves matter — a campaign can hit its goal with milestones outstanding.Acceptance criteria
total_donated(the issue's "metadata references the donation amount"), is readable afterwards, and updatesbalance/total_supplytransfer,transfer_from,approveall panic with#92, and a failed transfer leaves the receipt with its ownerTests — 13 new
transfer,transfer_from,approvepanic; receipt survives a failed transferFitting the codebase
New error codes go in a fresh 90s group (matching the existing 30s/40s/50s grouping);
DonationReceiptmirrorsDonorRecord; storage accessors followset_donor/get_donorincl. TTL bumping; the event topic mirrorsdonation_received. Test snapshots are committed, consistent with the 138 already tracked.One note for reviewers:
test_double_claim_panicsseeds the already-claimed state rather than callingclaim_receipttwice — tworequire_auth()calls for one address in a singleas_contractframe trip the auth mock ("frame is already authorized") before the guard is reached.test_claim_refund_already_claimedpre-seeds for the same reason.