diff --git a/contracts/admin/src/lib.rs b/contracts/admin/src/lib.rs index 211c9fb..ad4dc97 100644 --- a/contracts/admin/src/lib.rs +++ b/contracts/admin/src/lib.rs @@ -163,6 +163,105 @@ impl AdminContract { Ok(()) } + + /// Updates the stored payment contract address. + /// + /// # Parameters + /// - `admin`: the admin address that must be authorized. + /// - `payment_contract`: the new payment contract address. + /// + /// # Errors + /// Returns `Error::NotInitialized` if the admin contract has not been initialized, + /// and `Error::Unauthorized` if the provided admin address does not match the + /// stored admin. + pub fn set_payment_contract( + env: Env, + admin: Address, + payment_contract: Address, + ) -> Result<(), Error> { + admin.require_auth(); + + let stored_admin: Address = env + .storage() + .instance() + .get(&DataKey::Admin) + .ok_or(Error::NotInitialized)?; + if admin != stored_admin { + return Err(Error::Unauthorized); + } + + env.storage() + .instance() + .set(&DataKey::PaymentContract, &payment_contract); + + Ok(()) + } + + /// Updates the stored escrow contract address. + /// + /// # Parameters + /// - `admin`: the admin address that must be authorized. + /// - `escrow_contract`: the new escrow contract address. + /// + /// # Errors + /// Returns `Error::NotInitialized` if the admin contract has not been initialized, + /// and `Error::Unauthorized` if the provided admin address does not match the + /// stored admin. + pub fn set_escrow_contract( + env: Env, + admin: Address, + escrow_contract: Address, + ) -> Result<(), Error> { + admin.require_auth(); + + let stored_admin: Address = env + .storage() + .instance() + .get(&DataKey::Admin) + .ok_or(Error::NotInitialized)?; + if admin != stored_admin { + return Err(Error::Unauthorized); + } + + env.storage() + .instance() + .set(&DataKey::EscrowContract, &escrow_contract); + + Ok(()) + } + + /// Updates the stored refund contract address. + /// + /// # Parameters + /// - `admin`: the admin address that must be authorized. + /// - `refund_contract`: the new refund contract address. + /// + /// # Errors + /// Returns `Error::NotInitialized` if the admin contract has not been initialized, + /// and `Error::Unauthorized` if the provided admin address does not match the + /// stored admin. + pub fn set_refund_contract( + env: Env, + admin: Address, + refund_contract: Address, + ) -> Result<(), Error> { + admin.require_auth(); + + let stored_admin: Address = env + .storage() + .instance() + .get(&DataKey::Admin) + .ok_or(Error::NotInitialized)?; + if admin != stored_admin { + return Err(Error::Unauthorized); + } + + env.storage() + .instance() + .set(&DataKey::RefundContract, &refund_contract); + + Ok(()) + } } #[cfg(test)] diff --git a/contracts/payment/src/lib.rs b/contracts/payment/src/lib.rs index c92821d..3a54d11 100644 --- a/contracts/payment/src/lib.rs +++ b/contracts/payment/src/lib.rs @@ -65,6 +65,7 @@ pub enum PaymentKey { PendingSettlement(u64), AccumulatedFees, LargePaymentCounter, + Discount(u64), } pub const MAX_MEMO_VERSIONS: u32 = 10; @@ -915,6 +916,14 @@ pub struct ChannelOpened { pub amount: i128, } +#[contractevent] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ChannelToppedUp { + pub channel_id: u64, + pub amount: i128, + pub new_deposited: i128, +} + #[contractevent] #[derive(Clone, Debug, Eq, PartialEq)] pub struct ChannelSettled { @@ -3732,11 +3741,31 @@ impl PaymentContract { } } + // Subtract any loyalty-point discount redeemed against this payment (#490) + let discount: i128 = env + .storage() + .instance() + .get(&DataKey::Payment(PaymentKey::Discount(payment_id))) + .unwrap_or(0); + let charge_amount = if discount > 0 { + env.storage() + .instance() + .remove(&DataKey::Payment(PaymentKey::Discount(payment_id))); + let capped_discount = if discount > payment.amount { + payment.amount + } else { + discount + }; + payment.amount - capped_discount + } else { + payment.amount + }; + // Deduct platform fee (if configured) and get net amount for merchant let (net_amount, fee_amount) = PaymentContract::deduct_fee( env, payment_id, - payment.amount, + charge_amount, payment.merchant.clone(), &payment.token, &payment.customer, @@ -4194,6 +4223,16 @@ impl PaymentContract { &balance, ); + let existing_discount: i128 = env + .storage() + .instance() + .get(&DataKey::Payment(PaymentKey::Discount(payment_id))) + .unwrap_or(0); + env.storage().instance().set( + &DataKey::Payment(PaymentKey::Discount(payment_id)), + &(existing_discount + discount), + ); + Ok(discount) } @@ -10823,6 +10862,74 @@ impl PaymentContract { Ok(channel_id) } + /// Tops up an existing open payment channel with additional deposit. + /// + /// Allows a customer to add funds to a channel that has been drawn down, + /// avoiding the need to close and reopen a new channel for continued + /// micropayments. + /// + /// # Arguments + /// * `customer` - The channel's customer (must authorize). + /// * `channel_id` - The ID of the channel to top up. + /// * `amount` - Amount to add to the channel's deposit. + /// + /// # Returns + /// `Ok(())` on success. + /// + /// # Errors + /// Returns an error if the amount is non-positive, the channel is not found, + /// the caller is not the channel's customer, the channel is closed, or the + /// channel has expired. + pub fn top_up_channel( + env: Env, + customer: Address, + channel_id: u64, + amount: i128, + ) -> Result<(), Error> { + customer.require_auth(); + if amount <= 0 { + return Err(Error::Basic(BasicError::InvalidAmount)); + } + + let mut channel: PaymentChannel = env + .storage() + .instance() + .get(&DataKey::Feature(FeatureKey::PaymentChannel(channel_id))) + .ok_or(Error::Feature(FeatureError::ChannelNotFound))?; + + if channel.customer != customer { + return Err(Error::Basic(BasicError::Unauthorized)); + } + + if !channel.open { + return Err(Error::Feature(FeatureError::ChannelClosed)); + } + + if channel.expires_at > 0 && env.ledger().timestamp() > channel.expires_at { + return Err(Error::Feature(FeatureError::ChannelExpired)); + } + + let token_client = token::Client::new(&env, &channel.token); + let contract_address = env.current_contract_address(); + token_client.transfer(&customer, &contract_address, &amount); + + channel.deposited += amount; + + env.storage().instance().set( + &DataKey::Feature(FeatureKey::PaymentChannel(channel_id)), + &channel, + ); + + (ChannelToppedUp { + channel_id, + amount, + new_deposited: channel.deposited, + }) + .publish(&env); + + Ok(()) + } + /// Settles a payment channel with a signed off-chain state update. /// /// Verifies the customer's signature over (channel_id, merchant_amount, nonce), diff --git a/contracts/refund/src/lib.rs b/contracts/refund/src/lib.rs index 4c14b05..0f98a87 100644 --- a/contracts/refund/src/lib.rs +++ b/contracts/refund/src/lib.rs @@ -7959,6 +7959,12 @@ impl RefundContract { return Err(Error::Ext(ExtError::VoucherExpired)); } + token::Client::new(&env, &voucher.token).transfer( + &env.current_contract_address(), + &customer, + &voucher.amount, + ); + voucher.redeemed = true; env.storage() .instance()