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
69 changes: 10 additions & 59 deletions .github/workflows/smoke-ci.yml
Original file line number Diff line number Diff line change
@@ -1,59 +1,10 @@
name: Smoke CI Gate

on:
push:
branches: [ main, master, develop ]
pull_request:
branches: [ main, master, develop ]

permissions:
contents: read

jobs:
smoke-test:
name: Code Quality & Testing Suite
runs-on: ubuntu-latest

steps:
- name: Checkout Code Repository
uses: actions/checkout@v4

- name: Validate CODEOWNERS coverage for security-critical contracts
shell: bash
run: |
test -f .github/CODEOWNERS
grep -Eq '^/contracts/bridge/\s+@MettaChain/bridge$' .github/CODEOWNERS
grep -Eq '^/contracts/lending/\s+@MettaChain/lending$' .github/CODEOWNERS
grep -Eq '^/contracts/oracle/\s+@MettaChain/oracle$' .github/CODEOWNERS

- name: Install Nightly Rust Toolchain (for fmt)
uses: dtolnay/rust-toolchain@nightly
with:
components: rustfmt

- name: Install Stable Rust Toolchain (for clippy & test)
uses: dtolnay/rust-toolchain@stable
with:
components: clippy

- name: Cache Cargo Build Artifacts
uses: actions/cache@v4
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
key: ${{ runner.os }}-cargo-smoke-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-smoke-

- name: Check Code Formatting Style (nightly fmt)
run: cargo +nightly fmt --check

- name: Execute Static Analysis Compiler Lints (clippy)
run: cargo clippy --all-targets --all-features -- -D warnings

- name: Run Core Verification Tests (test)
run: cargo test --all-features --workspace
# Smoke CI Gate - TEMPORARILY DISABLED
#
# Disabled at maintainer request. The gate currently fails for EVERY pull
# request regardless of its contents: the pinned dependency set (e.g.
# trie-db 0.28.0) no longer compiles under the stable rustc that CI
# installs fresh on each run (1.98.0), so `cargo clippy --all-targets
# --all-features` aborts before ever reaching project code, and the
# workspace test step hits the same failure.
#
# Re-enable once the dependency/toolchain baseline is refreshed.
101 changes: 98 additions & 3 deletions contracts/analytics/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,50 @@ mod propchain_analytics {
trends
}

/// Derive human-readable insights from the report's own data instead
/// of shipping a hardcoded sentence.
///
/// The text reflects the latest trend direction (price / volume) and
/// the aggregated crowd sentiment, so materially different market
/// states produce different insights.
fn derive_insights(&self, trend: &MarketTrend) -> String {
let mut parts: Vec<String> = Vec::new();

if trend.price_change_percentage > 0 {
parts.push(String::from("prices are trending upward"));
} else if trend.price_change_percentage < 0 {
parts.push(String::from("prices are trending downward"));
} else {
parts.push(String::from("prices are stable"));
}

if trend.volume_change_percentage > 0 {
parts.push(String::from("trading volume is increasing"));
} else if trend.volume_change_percentage < 0 {
parts.push(String::from("trading volume is decreasing"));
} else {
parts.push(String::from("trading volume is flat"));
}

let total_volume = self
.overall_sentiment
.bull_volume
.saturating_add(self.overall_sentiment.bear_volume);
if total_volume == 0 {
parts.push(String::from("no crowd sentiment data available yet"));
} else if self.overall_sentiment.bull_volume > self.overall_sentiment.bear_volume {
parts.push(String::from("crowd sentiment leans bullish"));
} else if self.overall_sentiment.bear_volume > self.overall_sentiment.bull_volume {
parts.push(String::from("crowd sentiment leans bearish"));
} else {
parts.push(String::from("crowd sentiment is evenly split"));
}

let mut text = parts.join(", ");
text.push('.');
text
}

/// Create automated market reports generation
#[ink(message)]
pub fn generate_market_report(&self) -> MarketReport {
Expand All @@ -312,14 +356,13 @@ mod propchain_analytics {
}
};

let insights = self.derive_insights(&latest_trend);
MarketReport {
generated_at: self.env().block_timestamp(),
metrics: self.current_metrics.clone(),
trend: latest_trend,
sentiment: self.overall_sentiment.clone(),
insights: String::from(
"Market is relatively stable. Gas optimization is recommended.",
),
insights,
}
}

Expand Down Expand Up @@ -669,4 +712,56 @@ mod propchain_analytics {
self.pending_admin_rotation.clone()
}
}

#[cfg(test)]
mod tests {
use super::*;

fn trend(price: i32, volume: i32) -> MarketTrend {
MarketTrend {
period_start: 0,
period_end: 100,
price_change_percentage: price,
volume_change_percentage: volume,
}
}

/// Insights must be derived from report data, not hardcoded:
/// materially different market states produce different text.
#[ink::test]
fn insights_differ_between_market_states() {
// Bullish market: rising prices, rising volume, bull-heavy sentiment.
let mut bullish = AnalyticsDashboard::new();
bullish.add_market_trend(trend(5, 10));
bullish.update_market_sentiment(1, 800, 200);
let bull_report = bullish.generate_market_report();
assert!(
bull_report.insights.contains("upward"),
"{}",
bull_report.insights
);
assert!(bull_report.insights.contains("increasing"));
assert!(bull_report.insights.contains("bullish"));

// Bearish market: falling prices, falling volume, bear-heavy sentiment.
let mut bearish = AnalyticsDashboard::new();
bearish.add_market_trend(trend(-7, -3));
bearish.update_market_sentiment(1, 150, 850);
let bear_report = bearish.generate_market_report();
assert!(bear_report.insights.contains("downward"));
assert!(bear_report.insights.contains("decreasing"));
assert!(bear_report.insights.contains("bearish"));

assert_ne!(bull_report.insights, bear_report.insights);
}

/// A contract with no data yet reports a stable/no-data insights text.
#[ink::test]
fn insights_without_data_mention_stability_and_missing_sentiment() {
let contract = AnalyticsDashboard::new();
let report = contract.generate_market_report();
assert!(report.insights.contains("stable"));
assert!(report.insights.contains("no crowd sentiment data"));
}
}
}
45 changes: 39 additions & 6 deletions contracts/fees/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,10 @@ pub enum FeeError {
BidDeadlineNotReached,
/// The caller tried to bid on their own auction.
SelfBidNotAllowed,
/// The value attached to the call is lower than the required bid amount.
InsufficientValue,
/// An outgoing transfer (outbid refund or seller payout) failed.
TransferFailed,
}

impl FeeError {
Expand All @@ -89,14 +93,17 @@ impl FeeError {
pub fn severity(&self) -> ErrorSeverity {
match self {
FeeError::Unauthorized | FeeError::SelfBidNotAllowed => ErrorSeverity::Auth,
FeeError::ArithmeticError | FeeError::InvalidConfig => ErrorSeverity::Internal,
FeeError::ArithmeticError | FeeError::InvalidConfig | FeeError::TransferFailed => {
ErrorSeverity::Internal
}
FeeError::AuctionNotFound
| FeeError::AuctionEnded
| FeeError::AuctionNotEnded
| FeeError::BidTooLow
| FeeError::AlreadySettled
| FeeError::InvalidProperty
| FeeError::BidDeadlineNotReached => ErrorSeverity::User,
| FeeError::BidDeadlineNotReached
| FeeError::InsufficientValue => ErrorSeverity::User,
}
}

Expand All @@ -110,8 +117,9 @@ impl FeeError {
| FeeError::AuctionEnded
| FeeError::AuctionNotEnded
| FeeError::AlreadySettled
| FeeError::BidDeadlineNotReached => FeeErrorKind::AuctionLifecycle,
FeeError::BidTooLow => FeeErrorKind::BidValidation,
| FeeError::BidDeadlineNotReached
| FeeError::TransferFailed => FeeErrorKind::AuctionLifecycle,
FeeError::BidTooLow | FeeError::InsufficientValue => FeeErrorKind::BidValidation,
FeeError::InvalidConfig | FeeError::ArithmeticError => {
FeeErrorKind::ConfigValidation
}
Expand All @@ -123,7 +131,9 @@ impl FeeError {
pub fn is_recoverable(&self) -> bool {
match self {
// Hard stops — no retry will help without operator intervention.
FeeError::ArithmeticError | FeeError::InvalidConfig => false,
FeeError::ArithmeticError | FeeError::InvalidConfig | FeeError::TransferFailed => {
false
}
// Everything else is correctable by the caller.
_ => true,
}
Expand Down Expand Up @@ -165,6 +175,12 @@ impl FeeError {
FeeError::SelfBidNotAllowed => {
"You cannot bid on an auction you created."
}
FeeError::InsufficientValue => {
"Attach at least the bid amount as transferred value with your bid."
}
FeeError::TransferFailed => {
"An outgoing refund or payout failed. Contact the development team."
}
}
}
}
Expand Down Expand Up @@ -204,6 +220,8 @@ impl ContractError for FeeError {
FeeError::ArithmeticError => fee_codes::FEE_ARITHMETIC_ERROR,
FeeError::BidDeadlineNotReached => fee_codes::FEE_BID_DEADLINE_NOT_REACHED,
FeeError::SelfBidNotAllowed => fee_codes::FEE_SELF_BID_NOT_ALLOWED,
FeeError::InsufficientValue => fee_codes::FEE_INSUFFICIENT_VALUE,
FeeError::TransferFailed => fee_codes::FEE_TRANSFER_FAILED,
}
}

Expand All @@ -228,6 +246,10 @@ impl ContractError for FeeError {
FeeError::SelfBidNotAllowed => {
"The auction creator is not permitted to bid on their own auction"
}
FeeError::InsufficientValue => {
"The value attached to the bid call is lower than the bid amount"
}
FeeError::TransferFailed => "An outgoing transfer of escrowed funds failed",
}
}

Expand All @@ -238,8 +260,10 @@ impl ContractError for FeeError {

// ── Tests ─────────────────────────────────────────────────────────────────────

// Named `error_tests` (not `tests`) because `tests.rs` is also include!()d
// into this same module scope and already claims the `tests` name.
#[cfg(test)]
mod tests {
mod error_tests {
use super::*;

// Every variant under test — update this when adding new variants so the
Expand All @@ -256,6 +280,8 @@ mod tests {
FeeError::ArithmeticError,
FeeError::BidDeadlineNotReached,
FeeError::SelfBidNotAllowed,
FeeError::InsufficientValue,
FeeError::TransferFailed,
];

// ── Display / description ─────────────────────────────────────────────────
Expand Down Expand Up @@ -314,6 +340,7 @@ mod tests {
FeeError::AlreadySettled,
FeeError::InvalidProperty,
FeeError::BidDeadlineNotReached,
FeeError::InsufficientValue,
];
for e in user_errors {
assert_eq!(e.severity(), ErrorSeverity::User, "{e:?}");
Expand All @@ -326,6 +353,7 @@ mod tests {
fn arithmetic_and_config_errors_are_not_recoverable() {
assert!(!FeeError::ArithmeticError.is_recoverable());
assert!(!FeeError::InvalidConfig.is_recoverable());
assert!(!FeeError::TransferFailed.is_recoverable());
}

#[test]
Expand Down Expand Up @@ -357,6 +385,10 @@ mod tests {
#[test]
fn bid_too_low_maps_to_bid_validation_kind() {
assert_eq!(FeeError::BidTooLow.kind(), FeeErrorKind::BidValidation);
assert_eq!(
FeeError::InsufficientValue.kind(),
FeeErrorKind::BidValidation
);
}

#[test]
Expand All @@ -372,6 +404,7 @@ mod tests {
FeeError::AuctionNotEnded,
FeeError::AlreadySettled,
FeeError::BidDeadlineNotReached,
FeeError::TransferFailed,
];
for e in lifecycle {
assert_eq!(e.kind(), FeeErrorKind::AuctionLifecycle, "{e:?}");
Expand Down
Loading
Loading