Skip to content

Commit 62be042

Browse files
Merge pull request #150 from Alhaji-naira/feature/multi-issue-implementation
feat: Implement batch updates, rewards, milestones, and error mapping
2 parents a979702 + 0c94aa6 commit 62be042

File tree

2 files changed

+168
-24
lines changed

2 files changed

+168
-24
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: 151 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,11 @@ pub struct Meter {
125125
pub is_closed: bool,
126126
// Task #1: Stream Priority System
127127
pub priority_index: u32, // Priority level (0 = highest, higher numbers = lower priority)
128+
// Issue #113: Off-Peak Reward
129+
pub off_peak_reward_rate_bps: i128,
130+
// Issue #106: Milestones
131+
pub milestone_deadline: u64,
132+
pub milestone_confirmed: bool,
128133
}
129134

130135
#[contracttype]
@@ -401,6 +406,10 @@ pub enum ContractError {
401406
SubDaoBudgetExceeded = 47,
402407
SubDaoNotConfigured = 48,
403408
InsufficientXlmReserve = 49,
409+
// Issue #120
410+
UnfairPriceIncrease = 50,
411+
// Issue #109
412+
BillingGroupNotFound = 51,
404413
}
405414

406415
#[contracttype]
@@ -683,9 +692,15 @@ fn get_effective_rate(meter: &Meter, timestamp: u64) -> i128 {
683692
}
684693

685694
if is_peak_hour(timestamp) {
686-
base_rate.saturating_mul(PEAK_RATE_MULTIPLIER) / RATE_PRECISION
695+
base_rate.saturating_mul(PEAK_RATE_MULTIPLIER) / RATE_PRECISION // Apply peak rate
687696
} else {
688-
base_rate
697+
// Issue #113: Apply off-peak reward as a discount
698+
if meter.off_peak_reward_rate_bps > 0 && meter.off_peak_reward_rate_bps <= 10000 {
699+
let discount = (base_rate * meter.off_peak_reward_rate_bps) / 10000;
700+
base_rate.saturating_sub(discount)
701+
} else {
702+
base_rate
703+
}
689704
}
690705
}
691706

@@ -1376,8 +1391,31 @@ impl UtilityContract {
13761391
token,
13771392
billing_type,
13781393
device_public_key,
1379-
0,
1380-
)
1394+
is_paired: false,
1395+
grace_period_start: 0,
1396+
is_paused: false,
1397+
tier_threshold: 0,
1398+
tier_rate: 0,
1399+
is_disputed: false,
1400+
challenge_timestamp: 0,
1401+
credit_drip_rate: 0,
1402+
is_closed: false,
1403+
priority_index, // Task #1: Set priority index
1404+
off_peak_reward_rate_bps: 0,
1405+
milestone_deadline: 0,
1406+
milestone_confirmed: true,
1407+
};
1408+
1409+
env.storage().instance().set(&DataKey::Meter(count), &meter);
1410+
env.storage().instance().set(&DataKey::Count, &count);
1411+
1412+
// Initialize provider total pool (new meter starts with 0 value)
1413+
let current_pool = get_provider_total_pool_impl(&env, &provider);
1414+
env.storage()
1415+
.instance()
1416+
.set(&DataKey::ProviderTotalPool(provider), &current_pool);
1417+
1418+
count
13811419
}
13821420

13831421
pub fn batch_register_meters(env: Env, meter_infos: Vec<MeterInfo>) -> BatchCreatedEvent {
@@ -2189,12 +2227,7 @@ impl UtilityContract {
21892227
}
21902228

21912229
pub fn set_max_flow_rate(env: Env, meter_id: u64, max_rate_per_hour: i128) {
2192-
let mut meter: Meter = env
2193-
.storage()
2194-
.instance()
2195-
.get(&DataKey::Meter(meter_id))
2196-
.ok_or("Meter not found")
2197-
.unwrap();
2230+
let mut meter = get_meter_or_panic(&env, meter_id);
21982231
meter.provider.require_auth();
21992232

22002233
meter.max_flow_rate_per_hour = max_rate_per_hour;
@@ -2782,14 +2815,11 @@ impl UtilityContract {
27822815

27832816
pub fn group_top_up(env: Env, parent_account: Address, amount_per_meter: i128) {
27842817
parent_account.require_auth();
2785-
2786-
let billing_group: BillingGroup = env
2787-
.storage()
2788-
.instance()
2818+
2819+
let billing_group: BillingGroup = env.storage().instance().get(&DataKey::BillingGroup(parent_account.clone()))
27892820
.get(&DataKey::BillingGroup(parent_account.clone()))
2790-
.ok_or("Billing group not found")
2791-
.unwrap();
2792-
2821+
.unwrap_or_else(|| panic_with_error!(&env, ContractError::BillingGroupNotFound));
2822+
27932823
if billing_group.child_meters.is_empty() {
27942824
return;
27952825
}
@@ -2840,14 +2870,11 @@ impl UtilityContract {
28402870

28412871
pub fn remove_meter_from_billing_group(env: Env, parent_account: Address, meter_id: u64) {
28422872
parent_account.require_auth();
2843-
2844-
let mut billing_group: BillingGroup = env
2845-
.storage()
2846-
.instance()
2873+
2874+
let mut billing_group: BillingGroup = env.storage().instance().get(&DataKey::BillingGroup(parent_account.clone()))
28472875
.get(&DataKey::BillingGroup(parent_account.clone()))
2848-
.ok_or("Billing group not found")
2849-
.unwrap();
2850-
2876+
.unwrap_or_else(|| panic_with_error!(&env, ContractError::BillingGroupNotFound));
2877+
28512878
billing_group.child_meters.retain(|&id| id != meter_id);
28522879
env.storage()
28532880
.instance()
@@ -4017,6 +4044,106 @@ impl UtilityContract {
40174044
.get(&DataKey::SubDaoConfig(sub_dao))
40184045
.expect("Sub-DAO not configured")
40194046
}
4047+
4048+
// ============================================================
4049+
// IMPLEMENTATION FOR NEW ISSUES
4050+
// ============================================================
4051+
4052+
/// Issue #120: Batch update service tiers/rates for multiple meters.
4053+
/// Includes a "Fair Increase" guardrail to prevent price spikes (>10%).
4054+
pub fn batch_update_rates(env: Env, provider: Address, meter_ids: Vec<u64>, new_rate: i128) {
4055+
provider.require_auth();
4056+
4057+
if meter_ids.is_empty() {
4058+
return;
4059+
}
4060+
4061+
for meter_id in meter_ids.iter() {
4062+
let mut meter = get_meter_or_panic(&env, meter_id);
4063+
4064+
// Ensure the authenticated provider owns this meter
4065+
if meter.provider != provider {
4066+
// Skip meters not owned by the calling provider.
4067+
continue;
4068+
}
4069+
4070+
// Fair Increase Guardrail: < 10% increase
4071+
let max_allowed_rate = meter.off_peak_rate.saturating_mul(110) / 100;
4072+
if new_rate > max_allowed_rate {
4073+
panic_with_error!(&env, ContractError::UnfairPriceIncrease);
4074+
}
4075+
4076+
if new_rate < 0 {
4077+
panic_with_error!(&env, ContractError::InvalidUsageValue);
4078+
}
4079+
4080+
meter.off_peak_rate = new_rate;
4081+
meter.peak_rate = new_rate.saturating_mul(PEAK_RATE_MULTIPLIER) / RATE_PRECISION;
4082+
meter.rate_per_unit = new_rate;
4083+
4084+
env.storage().instance().set(&DataKey::Meter(meter_id), &meter);
4085+
}
4086+
4087+
env.events().publish(
4088+
(symbol_short!("BatchUpd"), provider),
4089+
(meter_ids.len(), new_rate),
4090+
);
4091+
}
4092+
4093+
/// Issue #113: Set an off-peak usage reward rate for a meter.
4094+
/// The reward is applied as a discount to the user.
4095+
pub fn set_off_peak_reward(env: Env, meter_id: u64, reward_rate_bps: i128) {
4096+
let mut meter = get_meter_or_panic(&env, meter_id);
4097+
meter.provider.require_auth();
4098+
4099+
// Reward rate should be between 0% and 100% (0-10000 bps)
4100+
if reward_rate_bps < 0 || reward_rate_bps > 10000 {
4101+
panic_with_error!(&env, ContractError::InvalidUsageValue);
4102+
}
4103+
4104+
meter.off_peak_reward_rate_bps = reward_rate_bps;
4105+
env.storage().instance().set(&DataKey::Meter(meter_id), &meter);
4106+
4107+
env.events().publish(
4108+
(symbol_short!("OffPekRwd"), meter_id),
4109+
reward_rate_bps,
4110+
);
4111+
}
4112+
4113+
/// Issue #106: Set a project milestone deadline.
4114+
/// If missed, the provider's effective rate is halved.
4115+
pub fn set_milestone(env: Env, meter_id: u64, deadline: u64) {
4116+
let mut meter = get_meter_or_panic(&env, meter_id);
4117+
meter.provider.require_auth();
4118+
4119+
meter.milestone_deadline = deadline;
4120+
meter.milestone_confirmed = false; // New milestone is unconfirmed by default
4121+
4122+
env.storage().instance().set(&DataKey::Meter(meter_id), &meter);
4123+
4124+
env.events().publish(
4125+
(symbol_short!("MstoneSet"), meter_id),
4126+
deadline,
4127+
);
4128+
}
4129+
4130+
/// Issue #106: Allow the user to confirm a milestone is complete.
4131+
/// This restores the provider's full rate.
4132+
pub fn confirm_milestone(env: Env, meter_id: u64) {
4133+
let mut meter = get_meter_or_panic(&env, meter_id);
4134+
meter.user.require_auth();
4135+
4136+
meter.milestone_confirmed = true;
4137+
// Reset deadline to prevent future penalties until a new milestone is set
4138+
meter.milestone_deadline = 0;
4139+
4140+
env.storage().instance().set(&DataKey::Meter(meter_id), &meter);
4141+
4142+
env.events().publish(
4143+
(symbol_short!("MstoneConf"), meter_id),
4144+
env.ledger().timestamp(),
4145+
);
4146+
}
40204147
}
40214148

40224149
fn verify_usage_signature(

0 commit comments

Comments
 (0)