Skip to content

[LOW] get_campaign_status::days_remaining truncates sub-day deadlines to 0 (UX / dashboard) #45

@Alqku

Description

@Alqku

Overview

CampaignStatusResponse.days_remaining (campaign/src/contract.rs lines 110–116, type in types.rs) returns an integer-day count computed by simple (end_time - now) / 86_400 truncation. Any deadline less than one full day away is reported as 0 ("less than a day remaining"), which a UI or dashboard is then forced to display either as "0 days" (confusing) or to re-engineer the precision client-side. Several test snapshots depend on this truncation, so the fix needs to either change the field semantics or layer in a separate sub-day view.

Evidence

// campaign/src/contract.rs (get_campaign_status)
let now = env.ledger().timestamp();
let days_remaining = if now < campaign.end_time {
    ((campaign.end_time - now) / 86_400) as i64          // truncates < 1 day to 0
} else {
    -(((now - campaign.end_time) / 86_400) as i64)
};

CampaignStatusResponse { status, days_remaining }

Test snapshots in campaign/test_snapshots/get_campaign_status/ exercise values where this integer-day "0" is exactly the response — a fix that adds sub-day granularity will require concurrent snapshot updates. Adding hours_remaining alongside days_remaining is the only non-breaking option.

Impact

  • Users checking "how long do I have until the deadline" see "0" for the entire final day, indistinguishable from "expired".
  • UI layers that want sub-day urgency cannot read it from this API.
  • Off-chain indexers that join get_campaign_status to render timers must compute the granular value independently of the contract, defeating the point of the typed view.

Recommended Approach

Make the field sub-day-capable without breaking the existing typed i64 shape — add a parallel hours_remaining_since_days: u32 (clamped to [0, 23]) or, preferably, change the units so CampaignStatusResponse reports seconds_remaining: i64 and let UI floor-to-days. Second option requires a snapshot refresh; first is additive.

Option A (preferred, additive):

pub struct CampaignStatusResponse {
    pub status: CampaignStatus,
    pub days_remaining: i64,        // existing semantics, retained for back-compat
    pub hours_remaining: u32,      // new: 0..=23 for positive deadlines; 0 for past deadlines
}
// campaign/src/contract.rs
let delta = (campaign.end_time as i64) - (now as i64);
let days_remaining = delta / 86_400;
let hours_remaining = if delta >= 0 { ((delta % 86_400) / 3_600) as u32 } else { 0 };

Option B (breaking replacement of days_remaining with seconds_remaining): cleaner long-term but requires updating all campaign/test_snapshots/ fixture files; track as a separate migration issue.

Acceptance Criteria

  • Sub-day deadline is distinguishable from "expired" through the contract API
  • Either hours_remaining is added to CampaignStatusResponse, or days_remaining is replaced with seconds_remaining (Option B)
  • Existing tests pass under Option A; under Option B, snapshots are regenerated
  • docs/events.md and any UI references are updated to read the new field
  • Negative days_remaining still distinguishable (deadline has passed) in both options

Affected Files

  • campaign/src/contract.rs
  • campaign/src/types.rs
  • campaign/test_snapshots/get_campaign_status/
  • docs/events.md (if event payload changes — likely not in Option A)

Metadata

Metadata

Assignees

Labels

GrantFox OSSIssue tracked in GrantFox OSSMaybe RewardedIssue may be eligible for a GrantFox rewardOfficial CampaignCampaign: Official CampaigndocumentationImprovements or additions to documentationgood first issueGood for newcomers

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions