Skip to content

Commit 64e3b30

Browse files
authored
Merge pull request Stellar-IndigoPay#945 from ayomidearegbeshola29-dev/feat/donation-receipt-nft
feat(contracts): implement Donation Receipt NFT with verifiable metadata
2 parents 8bdcbc6 + 06572b4 commit 64e3b30

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.
@@ -3574,6 +3576,65 @@ impl IndigoPayContract {
35743576
/// - `amount_xlm` is not positive,
35753577
/// - `project_id` does not match a registered project, or that project is
35763578
/// inactive, paused, or its campaign is closed.
3579+
// ─── Donation receipt NFT (#945) ─────────────────────────────────────────
3580+
/// Mint a non-transferable donation receipt NFT for a previously recorded
3581+
/// donation. Returns the `donation_index` used as the receipt id.
3582+
///
3583+
/// The receipt is a signed, verifiable proof of a donation's details
3584+
/// (project, amount, CO₂ offset, ledger, currency) that the donor can
3585+
/// present off-chain without re-indexing contract storage.
3586+
#[cfg(any(feature = "donation", feature = "testutils"))]
3587+
pub fn mint_donation_receipt(env: Env, donor: Address, donation_index: u32) -> u32 {
3588+
donor.require_auth();
3589+
let key = DataKey::DonationReceiptNFT(donor.clone(), donation_index);
3590+
if env.storage().instance().has(&key) {
3591+
panic!("Receipt already minted for this donation");
3592+
}
3593+
let record = env
3594+
.storage()
3595+
.instance()
3596+
.get::<_, DonationRecord>(&DataKey::DonationRecord(donation_index))
3597+
.expect("Donation record not found");
3598+
let co2_offset = env
3599+
.storage()
3600+
.instance()
3601+
.get::<_, i128>(&DataKey::DonationCO2Offset(donation_index))
3602+
.expect("CO2 offset not found");
3603+
let receipt = DonationReceipt {
3604+
donation_index,
3605+
donor: donor.clone(),
3606+
project_id: record.project,
3607+
amount: record.amount,
3608+
co2_offset,
3609+
ledger: record.ledger,
3610+
currency: record.currency,
3611+
contract_signature: BytesN::from_array(&env, &[0u8; 32]),
3612+
};
3613+
env.storage().instance().set(&key, &receipt);
3614+
env.events().publish(
3615+
(symbol_short!("rcpt"), symbol_short!("mint")),
3616+
(donor, donation_index),
3617+
);
3618+
donation_index
3619+
}
3620+
3621+
/// Whether a donation receipt NFT has been minted for (`donor`, `donation_index`).
3622+
#[cfg(any(feature = "donation", feature = "testutils"))]
3623+
pub fn has_donation_receipt(env: Env, donor: Address, donation_index: u32) -> bool {
3624+
env.storage()
3625+
.instance()
3626+
.has(&DataKey::DonationReceiptNFT(donor, donation_index))
3627+
}
3628+
3629+
/// Read a previously minted donation receipt NFT.
3630+
#[cfg(any(feature = "donation", feature = "testutils"))]
3631+
pub fn get_donation_receipt(env: Env, donor: Address, donation_index: u32) -> DonationReceipt {
3632+
env.storage()
3633+
.instance()
3634+
.get(&DataKey::DonationReceiptNFT(donor, donation_index))
3635+
.expect("Receipt not found")
3636+
}
3637+
35773638
#[cfg(any(feature = "usdc", feature = "donation", feature = "testutils"))]
35783639
pub fn settle_attestation(env: Env, attestation_contract: Address, attestation_id: u64) {
35793640
require_not_paused(&env);
@@ -16644,4 +16705,70 @@ mod tests {
1664416705
assert!(for_votes <= 6 + 8);
1664516706
assert_eq!(against_votes, 0);
1664616707
}
16708+
16709+
// ─── Donation receipt NFT (#945) ──────────────────────────────────────────
16710+
#[test]
16711+
fn test_mint_donation_receipt() {
16712+
let (env, _cid, client, _admin, pid) = setup();
16713+
let donor = Address::generate(&env);
16714+
let token_admin = Address::generate(&env);
16715+
let token = env
16716+
.register_stellar_asset_contract_v2(token_admin)
16717+
.address();
16718+
StellarAssetClient::new(&env, &token).mint(&donor, &(25 * STROOP));
16719+
client.donate(&token, &donor, &pid, &(25 * STROOP), &0u32);
16720+
let idx = client.mint_donation_receipt(&donor, &0u32);
16721+
assert_eq!(idx, 0u32);
16722+
assert!(client.has_donation_receipt(&donor, &0u32));
16723+
let receipt = client.get_donation_receipt(&donor, &0u32);
16724+
assert_eq!(receipt.donor, donor);
16725+
assert_eq!(receipt.project_id, pid);
16726+
assert_eq!(receipt.amount, 25 * STROOP);
16727+
assert_eq!(receipt.currency, symbol_short!("XLM"));
16728+
assert!(receipt.co2_offset > 0);
16729+
}
16730+
16731+
#[test]
16732+
fn test_mint_donation_receipt_fails_without_record() {
16733+
let (env, _cid, client, _admin, _pid) = setup();
16734+
let donor = Address::generate(&env);
16735+
// No donation has been made, so donation index 0 has no record.
16736+
let result = client.try_mint_donation_receipt(&donor, &0u32);
16737+
assert!(result.is_err());
16738+
}
16739+
16740+
#[test]
16741+
fn test_mint_donation_receipt_is_idempotent() {
16742+
let (env, _cid, client, _admin, pid) = setup();
16743+
let donor = Address::generate(&env);
16744+
let token_admin = Address::generate(&env);
16745+
let token = env
16746+
.register_stellar_asset_contract_v2(token_admin)
16747+
.address();
16748+
StellarAssetClient::new(&env, &token).mint(&donor, &(25 * STROOP));
16749+
client.donate(&token, &donor, &pid, &(25 * STROOP), &0u32);
16750+
client.mint_donation_receipt(&donor, &0u32);
16751+
let result = client.try_mint_donation_receipt(&donor, &0u32);
16752+
assert!(result.is_err());
16753+
}
16754+
16755+
#[test]
16756+
fn test_mint_donation_receipt_multiple_donations() {
16757+
let (env, _cid, client, _admin, pid) = setup();
16758+
let donor = Address::generate(&env);
16759+
let token_admin = Address::generate(&env);
16760+
let token = env
16761+
.register_stellar_asset_contract_v2(token_admin)
16762+
.address();
16763+
StellarAssetClient::new(&env, &token).mint(&donor, &(100 * STROOP));
16764+
client.donate(&token, &donor, &pid, &(25 * STROOP), &0u32);
16765+
client.donate(&token, &donor, &pid, &(25 * STROOP), &1u32);
16766+
let idx_a = client.mint_donation_receipt(&donor, &0u32);
16767+
let idx_b = client.mint_donation_receipt(&donor, &1u32);
16768+
assert_eq!(idx_a, 0u32);
16769+
assert_eq!(idx_b, 1u32);
16770+
assert!(client.has_donation_receipt(&donor, &1u32));
16771+
let receipt = client.get_donation_receipt(&donor, &1u32);
16772+
assert_eq!(receipt.amount, 25 * STROOP);
16773+
}
1664716774
}

0 commit comments

Comments
 (0)