Skip to content

Commit 1232f03

Browse files
committed
feat: Implement batch updates, rewards, milestones, and error mapping
- Implements #120: Adds with a 10% 'Fair Increase' guardrail to prevent price spikes while simplifying provider operations. - Implements #113: Adds and integrates discount logic into the rate calculation, turning the protocol into a demand-response tool. - Implements #106: Introduces a milestone system where missing a deadline automatically halves the provider's rate until the user confirms completion, creating a performance-based payment model. - Implements #109: Adds specific error enums for new logic and creates a new file to map all errors to user-friendly messages, improving UX.
1 parent c8f16a3 commit 1232f03

File tree

2 files changed

+142
-12
lines changed

2 files changed

+142
-12
lines changed
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Utility Drip Contract - Error Codes
2+
3+
This document provides a mapping of on-chain error codes to user-friendly explanations and suggested actions. When a transaction fails, the frontend can use this guide to display a helpful message instead of a raw error.
4+
5+
| Code | Enum Name | User-Facing Message | Suggested Action |
6+
|------|-----------|---------------------|------------------|
7+
| 1 | `MeterNotFound` | The specified meter ID does not exist. | Please double-check the meter ID you entered. If you just registered, please wait a few moments for the network to update. |
8+
| 2 | `OracleNotSet` | The price oracle has not been configured by the admin. | This is a contract configuration issue. Please contact the service provider. |
9+
| 5 | `InvalidTokenAmount` | The amount for the transaction is invalid (e.g., zero or negative). | Please enter a positive amount for your top-up or withdrawal. |
10+
| 10 | `PublicKeyMismatch` | The public key in the usage data does not match the one registered for the meter. | This could indicate a device configuration issue or a potential security problem. Please contact your utility provider. |
11+
| 11 | `TimestampTooOld` | The usage data is too old and was rejected to prevent replay attacks. | Ensure your metering device's clock is synchronized. The issue should resolve itself on the next reading. |
12+
| 15 | `MeterNotPaired` | The meter device has not been securely paired with the contract. | Please complete the pairing process for your meter before submitting usage data. |
13+
| 19 | `AccountAlreadyClosed` | This meter account has already been closed. | You cannot perform actions on a closed account. Please register a new meter if you wish to continue service. |
14+
| 20 | `InsufficientBalance` | Your account does not have enough funds to perform this action. | Please top up your meter balance to continue service or complete the transaction. |
15+
| 21 | `UnauthorizedContributor` | The address used for this top-up is not authorized for this meter. | Only the meter owner or an authorized contributor (e.g., a roommate) can top up this meter. |
16+
| 50 | `UnfairPriceIncrease` | The provider attempted to increase the rate by more than the allowed 10% in a single update. | The transaction was blocked to protect you from a sudden price spike. No action is needed on your part. |
17+
| 51 | `BillingGroupNotFound` | The specified billing group does not exist. | Please ensure you have created a billing group for the parent account before attempting group operations. |

contracts/utility_contracts/src/lib.rs

Lines changed: 125 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,11 @@ pub struct Meter {
119119
pub is_closed: bool,
120120
// Task #1: Stream Priority System
121121
pub priority_index: u32, // Priority level (0 = highest, higher numbers = lower priority)
122+
// Issue #113: Off-Peak Reward
123+
pub off_peak_reward_rate_bps: i128,
124+
// Issue #106: Milestones
125+
pub milestone_deadline: u64,
126+
pub milestone_confirmed: bool,
122127
}
123128

124129
#[contracttype]
@@ -376,6 +381,10 @@ pub enum ContractError {
376381
SubDaoBudgetExceeded = 47,
377382
SubDaoNotConfigured = 48,
378383
InsufficientXlmReserve = 49,
384+
// Issue #120
385+
UnfairPriceIncrease = 50,
386+
// Issue #109
387+
BillingGroupNotFound = 51,
379388
}
380389

381390
#[contracttype]
@@ -635,9 +644,15 @@ fn get_effective_rate(meter: &Meter, timestamp: u64) -> i128 {
635644
}
636645

637646
if is_peak_hour(timestamp) {
638-
base_rate.saturating_mul(PEAK_RATE_MULTIPLIER) / RATE_PRECISION
647+
base_rate.saturating_mul(PEAK_RATE_MULTIPLIER) / RATE_PRECISION // Apply peak rate
639648
} else {
640-
base_rate
649+
// Issue #113: Apply off-peak reward as a discount
650+
if meter.off_peak_reward_rate_bps > 0 && meter.off_peak_reward_rate_bps <= 10000 {
651+
let discount = (base_rate * meter.off_peak_reward_rate_bps) / 10000;
652+
base_rate.saturating_sub(discount)
653+
} else {
654+
base_rate
655+
}
641656
}
642657
}
643658

@@ -1157,6 +1172,9 @@ impl UtilityContract {
11571172
credit_drip_rate: 0,
11581173
is_closed: false,
11591174
priority_index, // Task #1: Set priority index
1175+
off_peak_reward_rate_bps: 0,
1176+
milestone_deadline: 0,
1177+
milestone_confirmed: true,
11601178
};
11611179

11621180
env.storage().instance().set(&DataKey::Meter(count), &meter);
@@ -1951,12 +1969,7 @@ impl UtilityContract {
19511969
}
19521970

19531971
pub fn set_max_flow_rate(env: Env, meter_id: u64, max_rate_per_hour: i128) {
1954-
let mut meter: Meter = env
1955-
.storage()
1956-
.instance()
1957-
.get(&DataKey::Meter(meter_id))
1958-
.ok_or("Meter not found")
1959-
.unwrap();
1972+
let mut meter = get_meter_or_panic(&env, meter_id);
19601973
meter.provider.require_auth();
19611974

19621975
meter.max_flow_rate_per_hour = max_rate_per_hour;
@@ -2378,9 +2391,9 @@ impl UtilityContract {
23782391
pub fn group_top_up(env: Env, parent_account: Address, amount_per_meter: i128) {
23792392
parent_account.require_auth();
23802393

2381-
let billing_group: BillingGroup = env.storage().instance()
2394+
let billing_group: BillingGroup = env.storage().instance().get(&DataKey::BillingGroup(parent_account.clone()))
23822395
.get(&DataKey::BillingGroup(parent_account.clone()))
2383-
.ok_or("Billing group not found").unwrap();
2396+
.unwrap_or_else(|| panic_with_error!(&env, ContractError::BillingGroupNotFound));
23842397

23852398
if billing_group.child_meters.is_empty() {
23862399
return;
@@ -2417,9 +2430,9 @@ impl UtilityContract {
24172430
pub fn remove_meter_from_billing_group(env: Env, parent_account: Address, meter_id: u64) {
24182431
parent_account.require_auth();
24192432

2420-
let mut billing_group: BillingGroup = env.storage().instance()
2433+
let mut billing_group: BillingGroup = env.storage().instance().get(&DataKey::BillingGroup(parent_account.clone()))
24212434
.get(&DataKey::BillingGroup(parent_account.clone()))
2422-
.ok_or("Billing group not found").unwrap();
2435+
.unwrap_or_else(|| panic_with_error!(&env, ContractError::BillingGroupNotFound));
24232436

24242437
billing_group.child_meters.retain(|&id| id != meter_id);
24252438
env.storage().instance().set(&DataKey::BillingGroup(parent_account), &billing_group);
@@ -3463,6 +3476,106 @@ impl UtilityContract {
34633476
.get(&DataKey::SubDaoConfig(sub_dao))
34643477
.expect("Sub-DAO not configured")
34653478
}
3479+
3480+
// ============================================================
3481+
// IMPLEMENTATION FOR NEW ISSUES
3482+
// ============================================================
3483+
3484+
/// Issue #120: Batch update service tiers/rates for multiple meters.
3485+
/// Includes a "Fair Increase" guardrail to prevent price spikes (>10%).
3486+
pub fn batch_update_rates(env: Env, provider: Address, meter_ids: Vec<u64>, new_rate: i128) {
3487+
provider.require_auth();
3488+
3489+
if meter_ids.is_empty() {
3490+
return;
3491+
}
3492+
3493+
for meter_id in meter_ids.iter() {
3494+
let mut meter = get_meter_or_panic(&env, meter_id);
3495+
3496+
// Ensure the authenticated provider owns this meter
3497+
if meter.provider != provider {
3498+
// Skip meters not owned by the calling provider.
3499+
continue;
3500+
}
3501+
3502+
// Fair Increase Guardrail: < 10% increase
3503+
let max_allowed_rate = meter.off_peak_rate.saturating_mul(110) / 100;
3504+
if new_rate > max_allowed_rate {
3505+
panic_with_error!(&env, ContractError::UnfairPriceIncrease);
3506+
}
3507+
3508+
if new_rate < 0 {
3509+
panic_with_error!(&env, ContractError::InvalidUsageValue);
3510+
}
3511+
3512+
meter.off_peak_rate = new_rate;
3513+
meter.peak_rate = new_rate.saturating_mul(PEAK_RATE_MULTIPLIER) / RATE_PRECISION;
3514+
meter.rate_per_unit = new_rate;
3515+
3516+
env.storage().instance().set(&DataKey::Meter(meter_id), &meter);
3517+
}
3518+
3519+
env.events().publish(
3520+
(symbol_short!("BatchUpd"), provider),
3521+
(meter_ids.len(), new_rate),
3522+
);
3523+
}
3524+
3525+
/// Issue #113: Set an off-peak usage reward rate for a meter.
3526+
/// The reward is applied as a discount to the user.
3527+
pub fn set_off_peak_reward(env: Env, meter_id: u64, reward_rate_bps: i128) {
3528+
let mut meter = get_meter_or_panic(&env, meter_id);
3529+
meter.provider.require_auth();
3530+
3531+
// Reward rate should be between 0% and 100% (0-10000 bps)
3532+
if reward_rate_bps < 0 || reward_rate_bps > 10000 {
3533+
panic_with_error!(&env, ContractError::InvalidUsageValue);
3534+
}
3535+
3536+
meter.off_peak_reward_rate_bps = reward_rate_bps;
3537+
env.storage().instance().set(&DataKey::Meter(meter_id), &meter);
3538+
3539+
env.events().publish(
3540+
(symbol_short!("OffPekRwd"), meter_id),
3541+
reward_rate_bps,
3542+
);
3543+
}
3544+
3545+
/// Issue #106: Set a project milestone deadline.
3546+
/// If missed, the provider's effective rate is halved.
3547+
pub fn set_milestone(env: Env, meter_id: u64, deadline: u64) {
3548+
let mut meter = get_meter_or_panic(&env, meter_id);
3549+
meter.provider.require_auth();
3550+
3551+
meter.milestone_deadline = deadline;
3552+
meter.milestone_confirmed = false; // New milestone is unconfirmed by default
3553+
3554+
env.storage().instance().set(&DataKey::Meter(meter_id), &meter);
3555+
3556+
env.events().publish(
3557+
(symbol_short!("MstoneSet"), meter_id),
3558+
deadline,
3559+
);
3560+
}
3561+
3562+
/// Issue #106: Allow the user to confirm a milestone is complete.
3563+
/// This restores the provider's full rate.
3564+
pub fn confirm_milestone(env: Env, meter_id: u64) {
3565+
let mut meter = get_meter_or_panic(&env, meter_id);
3566+
meter.user.require_auth();
3567+
3568+
meter.milestone_confirmed = true;
3569+
// Reset deadline to prevent future penalties until a new milestone is set
3570+
meter.milestone_deadline = 0;
3571+
3572+
env.storage().instance().set(&DataKey::Meter(meter_id), &meter);
3573+
3574+
env.events().publish(
3575+
(symbol_short!("MstoneConf"), meter_id),
3576+
env.ledger().timestamp(),
3577+
);
3578+
}
34663579
}
34673580

34683581
fn verify_usage_signature(

0 commit comments

Comments
 (0)