Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 99 additions & 0 deletions contracts/admin/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down
109 changes: 108 additions & 1 deletion contracts/payment/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ pub enum PaymentKey {
PendingSettlement(u64),
AccumulatedFees,
LargePaymentCounter,
Discount(u64),
}

pub const MAX_MEMO_VERSIONS: u32 = 10;
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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)
}

Expand Down Expand Up @@ -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),
Expand Down
6 changes: 6 additions & 0 deletions contracts/refund/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Loading