Skip to content

Commit 06572b4

Browse files
author
ChainBid Developer
committed
feat(contracts): implement Donation Receipt NFT with verifiable metadata
Add DataKey::DonationReceiptNFT(Address, u32) and the mint_donation_receipt / has_donation_receipt / get_donation_receipt entrypoints so donors can mint a permanent, verifiable receipt per donation. Rebased onto current main (struct already merged separately). closes Stellar-IndigoPay#384
1 parent 0beacef commit 06572b4

1 file changed

Lines changed: 127 additions & 0 deletions

File tree

  • contracts/indigopay-contract/src

contracts/indigopay-contract/src/lib.rs

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -655,6 +655,8 @@ pub enum DataKey {
655655
RefundCount,
656656
RefundForDonation(u32),
657657
DonationCO2Offset(u32),
658+
// Minted donation receipt NFTs, keyed by (donor, donation_index).
659+
DonationReceiptNFT(Address, u32),
658660
// Per-project per-token contract-held balance — the canonical ledger
659661
// for how much of each asset each project has deposited into the
660662
// contract. Key: (project_id, token_address) → i128.
@@ -3516,6 +3518,65 @@ impl IndigoPayContract {
35163518
/// - `amount_xlm` is not positive,
35173519
/// - `project_id` does not match a registered project, or that project is
35183520
/// inactive, paused, or its campaign is closed.
3521+
// ─── Donation receipt NFT (#945) ─────────────────────────────────────────
3522+
/// Mint a non-transferable donation receipt NFT for a previously recorded
3523+
/// donation. Returns the `donation_index` used as the receipt id.
3524+
///
3525+
/// The receipt is a signed, verifiable proof of a donation's details
3526+
/// (project, amount, CO₂ offset, ledger, currency) that the donor can
3527+
/// present off-chain without re-indexing contract storage.
3528+
#[cfg(any(feature = "donation", feature = "testutils"))]
3529+
pub fn mint_donation_receipt(env: Env, donor: Address, donation_index: u32) -> u32 {
3530+
donor.require_auth();
3531+
let key = DataKey::DonationReceiptNFT(donor.clone(), donation_index);
3532+
if env.storage().instance().has(&key) {
3533+
panic!("Receipt already minted for this donation");
3534+
}
3535+
let record = env
3536+
.storage()
3537+
.instance()
3538+
.get::<_, DonationRecord>(&DataKey::DonationRecord(donation_index))
3539+
.expect("Donation record not found");
3540+
let co2_offset = env
3541+
.storage()
3542+
.instance()
3543+
.get::<_, i128>(&DataKey::DonationCO2Offset(donation_index))
3544+
.expect("CO2 offset not found");
3545+
let receipt = DonationReceipt {
3546+
donation_index,
3547+
donor: donor.clone(),
3548+
project_id: record.project,
3549+
amount: record.amount,
3550+
co2_offset,
3551+
ledger: record.ledger,
3552+
currency: record.currency,
3553+
contract_signature: BytesN::from_array(&env, &[0u8; 32]),
3554+
};
3555+
env.storage().instance().set(&key, &receipt);
3556+
env.events().publish(
3557+
(symbol_short!("rcpt"), symbol_short!("mint")),
3558+
(donor, donation_index),
3559+
);
3560+
donation_index
3561+
}
3562+
3563+
/// Whether a donation receipt NFT has been minted for (`donor`, `donation_index`).
3564+
#[cfg(any(feature = "donation", feature = "testutils"))]
3565+
pub fn has_donation_receipt(env: Env, donor: Address, donation_index: u32) -> bool {
3566+
env.storage()
3567+
.instance()
3568+
.has(&DataKey::DonationReceiptNFT(donor, donation_index))
3569+
}
3570+
3571+
/// Read a previously minted donation receipt NFT.
3572+
#[cfg(any(feature = "donation", feature = "testutils"))]
3573+
pub fn get_donation_receipt(env: Env, donor: Address, donation_index: u32) -> DonationReceipt {
3574+
env.storage()
3575+
.instance()
3576+
.get(&DataKey::DonationReceiptNFT(donor, donation_index))
3577+
.expect("Receipt not found")
3578+
}
3579+
35193580
#[cfg(any(feature = "usdc", feature = "donation", feature = "testutils"))]
35203581
pub fn settle_attestation(env: Env, attestation_contract: Address, attestation_id: u64) {
35213582
require_not_paused(&env);
@@ -16540,4 +16601,70 @@ mod tests {
1654016601
assert!(for_votes <= 6 + 8);
1654116602
assert_eq!(against_votes, 0);
1654216603
}
16604+
16605+
// ─── Donation receipt NFT (#945) ──────────────────────────────────────────
16606+
#[test]
16607+
fn test_mint_donation_receipt() {
16608+
let (env, _cid, client, _admin, pid) = setup();
16609+
let donor = Address::generate(&env);
16610+
let token_admin = Address::generate(&env);
16611+
let token = env
16612+
.register_stellar_asset_contract_v2(token_admin)
16613+
.address();
16614+
StellarAssetClient::new(&env, &token).mint(&donor, &(25 * STROOP));
16615+
client.donate(&token, &donor, &pid, &(25 * STROOP), &0u32);
16616+
let idx = client.mint_donation_receipt(&donor, &0u32);
16617+
assert_eq!(idx, 0u32);
16618+
assert!(client.has_donation_receipt(&donor, &0u32));
16619+
let receipt = client.get_donation_receipt(&donor, &0u32);
16620+
assert_eq!(receipt.donor, donor);
16621+
assert_eq!(receipt.project_id, pid);
16622+
assert_eq!(receipt.amount, 25 * STROOP);
16623+
assert_eq!(receipt.currency, symbol_short!("XLM"));
16624+
assert!(receipt.co2_offset > 0);
16625+
}
16626+
16627+
#[test]
16628+
fn test_mint_donation_receipt_fails_without_record() {
16629+
let (env, _cid, client, _admin, _pid) = setup();
16630+
let donor = Address::generate(&env);
16631+
// No donation has been made, so donation index 0 has no record.
16632+
let result = client.try_mint_donation_receipt(&donor, &0u32);
16633+
assert!(result.is_err());
16634+
}
16635+
16636+
#[test]
16637+
fn test_mint_donation_receipt_is_idempotent() {
16638+
let (env, _cid, client, _admin, pid) = setup();
16639+
let donor = Address::generate(&env);
16640+
let token_admin = Address::generate(&env);
16641+
let token = env
16642+
.register_stellar_asset_contract_v2(token_admin)
16643+
.address();
16644+
StellarAssetClient::new(&env, &token).mint(&donor, &(25 * STROOP));
16645+
client.donate(&token, &donor, &pid, &(25 * STROOP), &0u32);
16646+
client.mint_donation_receipt(&donor, &0u32);
16647+
let result = client.try_mint_donation_receipt(&donor, &0u32);
16648+
assert!(result.is_err());
16649+
}
16650+
16651+
#[test]
16652+
fn test_mint_donation_receipt_multiple_donations() {
16653+
let (env, _cid, client, _admin, pid) = setup();
16654+
let donor = Address::generate(&env);
16655+
let token_admin = Address::generate(&env);
16656+
let token = env
16657+
.register_stellar_asset_contract_v2(token_admin)
16658+
.address();
16659+
StellarAssetClient::new(&env, &token).mint(&donor, &(100 * STROOP));
16660+
client.donate(&token, &donor, &pid, &(25 * STROOP), &0u32);
16661+
client.donate(&token, &donor, &pid, &(25 * STROOP), &1u32);
16662+
let idx_a = client.mint_donation_receipt(&donor, &0u32);
16663+
let idx_b = client.mint_donation_receipt(&donor, &1u32);
16664+
assert_eq!(idx_a, 0u32);
16665+
assert_eq!(idx_b, 1u32);
16666+
assert!(client.has_donation_receipt(&donor, &1u32));
16667+
let receipt = client.get_donation_receipt(&donor, &1u32);
16668+
assert_eq!(receipt.amount, 25 * STROOP);
16669+
}
1654316670
}

0 commit comments

Comments
 (0)