From e937a6a7f79581dc8cc1b46ac4be9c5dfd4243eb Mon Sep 17 00:00:00 2001 From: josunday002 Date: Sat, 25 Jul 2026 01:20:52 +0100 Subject: [PATCH 1/4] jobsunday002: add bitmap-based verifier approval storage helper Additive helper module for O(1) approval checks and 8-byte storage instead of a 1.6KB Vec
for 50 verifiers. Not yet wired into approve_and_mint. --- .../credit_registry/src/approvals_bitmap.rs | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 contracts/credit_registry/src/approvals_bitmap.rs diff --git a/contracts/credit_registry/src/approvals_bitmap.rs b/contracts/credit_registry/src/approvals_bitmap.rs new file mode 100644 index 0000000..41dd4e6 --- /dev/null +++ b/contracts/credit_registry/src/approvals_bitmap.rs @@ -0,0 +1,55 @@ +//! Bitmap-based verifier approval tracking (additive, not yet wired in). +//! +//! DataKey::CreditApprovals(credit_id) currently stores a `Vec
` of every +//! verifier who approved a credit. With 50 registered verifiers this Vec can grow +//! to ~1.6KB per credit, and membership checks in `approve_and_mint` are O(n). +//! +//! This module provides a drop-in replacement storage shape: a `u64` bitmap where +//! bit `i` represents the verifier at index `i` in the registry's verifier list +//! (see `storage::get_verifiers`). 50 verifiers fit in 8 bytes instead of 1.6KB, +//! and approval checks/inserts become O(1) bit operations. +//! +//! To adopt: replace `DataKey::CreditApprovals(BytesN<32>) -> Vec
` reads/ +//! writes in `approve_and_mint` with the helpers below, resolving each verifier's +//! index via `get_verifiers(env).iter().position(...)`. + +use soroban_sdk::{Address, Env, Vec}; + +/// Returns true if the verifier at `index` has already approved. +pub fn has_approved(bitmap: u64, index: u32) -> bool { + debug_assert!(index < 64); + (bitmap & (1u64 << index)) != 0 +} + +/// Returns a new bitmap with the verifier at `index` marked as approved. +pub fn set_approved(bitmap: u64, index: u32) -> u64 { + debug_assert!(index < 64); + bitmap | (1u64 << index) +} + +/// Number of verifiers who have approved so far. +pub fn approval_count(bitmap: u64) -> u32 { + bitmap.count_ones() +} + +/// Finds the index of `verifier` within the registry's verifier list. +pub fn verifier_index(env: &Env, verifiers: &Vec
, verifier: &Address) -> Option { + let _ = env; + verifiers.iter().position(|v| v == *verifier).map(|i| i as u32) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn bitmap_set_and_check() { + let bm = 0u64; + assert!(!has_approved(bm, 3)); + let bm = set_approved(bm, 3); + assert!(has_approved(bm, 3)); + assert_eq!(approval_count(bm), 1); + let bm = set_approved(bm, 10); + assert_eq!(approval_count(bm), 2); + } +} From 2baae2087487f8492f352a6e37b86430c6b710e9 Mon Sep 17 00:00:00 2001 From: josunday002 Date: Sat, 25 Jul 2026 01:21:02 +0100 Subject: [PATCH 2/4] jobsunday002: add contract-state smoke test script Verifies admin initialization and credit_registry->retirement wiring via the Soroban CLI, complementing the existing HTTP-only smoke test. --- scripts/smoke-test-contracts.sh | 49 +++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100755 scripts/smoke-test-contracts.sh diff --git a/scripts/smoke-test-contracts.sh b/scripts/smoke-test-contracts.sh new file mode 100755 index 0000000..2fc8b98 --- /dev/null +++ b/scripts/smoke-test-contracts.sh @@ -0,0 +1,49 @@ +#!/bin/bash +# Post-deployment contract-state smoke test (additive, standalone). +# +# Complements scripts/smoke-test.sh by verifying core contract wiring via the +# Soroban/Stellar CLI instead of HTTP endpoints: admin initialization on each +# contract, and that credit_registry is wired to the correct retirement +# contract address. Catches deployment regressions such as wrong contract +# wiring or missing admin initialization. +# +# Usage: scripts/smoke-test-contracts.sh + +set -euo pipefail + +CONTRACTS_FILE="${1:?Usage: $0 }" +NETWORK="${2:-testnet}" + +log() { echo " [contract-smoke] $*"; } +pass() { echo " ✅ $*"; } +fail() { echo " ❌ $*" >&2; exit 1; } + +command -v stellar >/dev/null 2>&1 || fail "stellar CLI not found on PATH" +[[ -f "$CONTRACTS_FILE" ]] || fail "Contracts file not found: $CONTRACTS_FILE" + +CREDIT_REGISTRY_ID=$(jq -r '.credit_registry' "$CONTRACTS_FILE") +RETIREMENT_ID=$(jq -r '.retirement' "$CONTRACTS_FILE") +MARKETPLACE_ID=$(jq -r '.marketplace' "$CONTRACTS_FILE") +VERIFIER_REGISTRY_ID=$(jq -r '.verifier_registry // .credit_registry' "$CONTRACTS_FILE") + +invoke() { + local contract_id="$1"; shift + stellar contract invoke --id "$contract_id" --network "$NETWORK" -- "$@" +} + +log "Checking admin is initialized on each contract..." +for name_id in "credit_registry:$CREDIT_REGISTRY_ID" "retirement:$RETIREMENT_ID" "marketplace:$MARKETPLACE_ID"; do + name="${name_id%%:*}" + id="${name_id##*:}" + [[ "$id" != "null" && -n "$id" ]] || fail "$name contract id missing from $CONTRACTS_FILE" + ADMIN=$(invoke "$id" get_admin 2>/dev/null || true) + [[ -n "$ADMIN" && "$ADMIN" != "null" ]] || fail "$name: admin not initialized" + pass "$name admin initialized ($ADMIN)" +done + +log "Checking credit_registry is wired to the correct retirement contract..." +WIRED_RETIREMENT=$(invoke "$CREDIT_REGISTRY_ID" get_retirement_contract 2>/dev/null || true) +[[ "$WIRED_RETIREMENT" == *"$RETIREMENT_ID"* ]] || fail "credit_registry.retirement_contract mismatch: got $WIRED_RETIREMENT, expected $RETIREMENT_ID" +pass "credit_registry -> retirement wiring correct" + +pass "All contract-state smoke checks passed." From 1036388e35f6e68f6b950151b08200ebfa6bf57a Mon Sep 17 00:00:00 2001 From: josunday002 Date: Sat, 25 Jul 2026 01:21:11 +0100 Subject: [PATCH 3/4] jobsunday002: add marketplace loading skeleton component Standalone shimmer skeleton to show while GET /api/v1/credits is in flight, avoiding a blank screen during the 1-2s initial fetch. --- .../marketplace/loading-skeleton.component.ts | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 frontend/src/app/marketplace/loading-skeleton.component.ts diff --git a/frontend/src/app/marketplace/loading-skeleton.component.ts b/frontend/src/app/marketplace/loading-skeleton.component.ts new file mode 100644 index 0000000..9f88ef9 --- /dev/null +++ b/frontend/src/app/marketplace/loading-skeleton.component.ts @@ -0,0 +1,43 @@ +import { Component, Input } from '@angular/core'; +import { CommonModule } from '@angular/common'; + +/** + * Standalone loading skeleton for the marketplace credit list. + * + * Additive component (not yet wired into marketplace.component.ts). Intended + * usage: show `` while the GET + * /api/v1/credits request is in flight, so the first paint isn't a blank + * white screen during the ~1-2s fetch. + */ +@Component({ + selector: 'app-marketplace-loading-skeleton', + standalone: true, + imports: [CommonModule], + template: ` +
+
+
+ `, + styles: [` + .skeleton-list { display: flex; flex-direction: column; gap: 12px; } + .skeleton-card { + height: 88px; + border-radius: 8px; + background: linear-gradient(90deg, #e8e8e8 25%, #f2f2f2 37%, #e8e8e8 63%); + background-size: 400% 100%; + animation: skeleton-shimmer 1.4s ease infinite; + } + @keyframes skeleton-shimmer { + 0% { background-position: 100% 50%; } + 100% { background-position: 0 50%; } + } + `], +}) +export class MarketplaceLoadingSkeletonComponent { + /** Number of placeholder rows to render while credits are loading. */ + @Input() count = 6; + + get rows(): number[] { + return Array.from({ length: this.count }, (_, i) => i); + } +} From f500e578ebee8b7831baefa27069e000cbebfba7 Mon Sep 17 00:00:00 2001 From: josunday002 Date: Sat, 25 Jul 2026 01:21:24 +0100 Subject: [PATCH 4/4] jobsunday002: add structured exception filter for API errors Additive NestJS ExceptionFilter returning { code, message, details } instead of the default unstructured body, enabling localized/ context-specific frontend error handling. --- .../filters/structured-exception.filter.ts | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 api/src/common/filters/structured-exception.filter.ts diff --git a/api/src/common/filters/structured-exception.filter.ts b/api/src/common/filters/structured-exception.filter.ts new file mode 100644 index 0000000..a941667 --- /dev/null +++ b/api/src/common/filters/structured-exception.filter.ts @@ -0,0 +1,66 @@ +import { + ArgumentsHost, + Catch, + ExceptionFilter, + HttpException, + HttpStatus, +} from '@nestjs/common'; +import { Response } from 'express'; + +/** + * Structured error response shape for API clients. + * + * Additive filter (not yet wired into main.ts / app.module.ts). Intended to + * replace NestJS's default unstructured exception body so the frontend can + * distinguish error kinds (e.g. "Invalid tonnes") and show localized, + * context-specific messages instead of a generic 400 handler. + * + * To adopt: register globally, e.g. in main.ts: + * app.useGlobalFilters(new StructuredExceptionFilter()); + */ +export interface StructuredErrorResponse { + code: string; + message: string; + details?: Record; +} + +@Catch(HttpException) +export class StructuredExceptionFilter implements ExceptionFilter { + catch(exception: HttpException, host: ArgumentsHost): void { + const ctx = host.switchToHttp(); + const response = ctx.getResponse(); + const status = exception.getStatus(); + const body = exception.getResponse(); + + const { code, message, details } = this.normalize(status, body); + + response.status(status).json({ code, message, details }); + } + + private normalize( + status: number, + body: string | object, + ): StructuredErrorResponse { + const code = HttpStatus[status] ?? 'ERROR'; + + if (typeof body === 'string') { + return { code, message: body }; + } + + const obj = body as Record; + const message = Array.isArray(obj.message) + ? obj.message.join('; ') + : ((obj.message as string) ?? 'An error occurred'); + + const details = { ...obj }; + delete details.message; + delete details.statusCode; + delete details.error; + + return { + code: typeof obj.error === 'string' ? obj.error : code, + message, + details: Object.keys(details).length ? details : undefined, + }; + } +}