Skip to content
Open
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@ target
*.rs.bk
.DS_Store
.env
temp_auto_push.bat
.env.*
/target/
/target_local/
.md-*
hidden
.gitignore
branch_structure.json
67 changes: 47 additions & 20 deletions contracts/registry/src/admin.rs
Original file line number Diff line number Diff line change
@@ -1,31 +1,41 @@
//! Admin cooldown enforcement for the Callora registry.
//!
//! Implements a per-developer cool-off window between registrations to prevent
//! rapid abuse by the same developer while allowing different developers to
//! register concurrently. Every admin-gated entrypoint in
//! [`crate::CalloraRegistry`] must call [`require_cooldown`] before
//! mutating state and [`update_cooldown`] after a successful mutation.
//!
//! # Storage
//!
//! The last-registration timestamp for each developer is stored in persistent
//! storage under [`crate::StorageKey::DeveloperCooldown`]`(developer)`.
//! There is exactly one authoritative key per developer; the former global
//! `Symbol`-based key has been removed.
///
/// Implements a per-developer cool-off window between registrations to prevent
/// rapid abuse by the same developer while allowing different developers to
/// register concurrently. Every admin-gated entrypoint in
/// [`crate::CalloraRegistry`] must call [[`require_cooldown`] before
/// mutating state and [[`update_cooldown`] after a successful mutation.
///
/// # Storage
///
/// The last-registration timestamp for each developer is stored in persistent
/// storage under [`crate::StorageKey::DeveloperCooldown`]`(developer)`.
/// There is exactly one authoritative key per developer; the former global
/// `Symbol`-based key has been removed.
///
/// # TTL
///
/// Cooldown markers are extended on every write and on every successful read
/// so they cannot archive while the cooldown window is still effective. If a
/// marker did archive, the next registration would either fail until the entry
/// is restored or silently skip the cooldown after expiry.

use soroban_sdk::{Address, Env};

use crate::{RegistryError, StorageKey};
use crate::{RegistryError, StorageKey, COOLDOWN_TTL_EXTENSION, COOLDOWN_TTL_THRESHOLD};

/// Cooldown window in seconds between registrations by the same developer.
///
/// Set to 3 600 (1 hour): the same developer must wait at least this long
/// after one registration before performing another one. Different developers
/// are unaffected by each other's cooldown windows.
pub const COOLDOWN_SECONDS: u64 = 3_600;
pub const COOLDOWN_SECONDS: u64 = 3,600;

/// Return the ledger timestamp of the last registration by `developer`, or
/// `None` if this developer has not yet registered anything.
///
/// This is a pure read helper and does not extend the TTL. Callers
/// (`require_cooldown`) handle the extension so the marker stays live.
pub fn last_developer_action(env: &Env, developer: &Address) -> Option<u64> {
env.storage()
.persistent()
Expand All @@ -35,11 +45,21 @@ pub fn last_developer_action(env: &Env, developer: &Address) -> Option<u64> {
/// Assert that the cooldown window has elapsed since `developer`'s last
/// registration.
///
/// Returns `Err(RegistryError::AdminCooldownActive)` if the cooldown has not
/// Returns `Err(RegistryError::AdminCooldownActive)if the cooldown has not
/// expired for this developer. This is a no-op when the developer has no prior
/// registration, so every developer's first action always succeeds.
///
/// When a marker is present, its TT\ is extended on read so the cooldown
/// state cannot archive out from under an active window.
pub fn require_cooldown(env: &Env, developer: &Address) -> Result<(), RegistryError> {
if let Some(last) = last_developer_action(env, developer) {
let key = StorageKey::DeveloperCooldown(developer.clone());
if let Some(last) = env.storage().persistent().get::<u64>(&key) {
// Extend on read so a live marker never archives between registrations.
env.storage().persistent().extend_ttl(
&key,
COOLDOWN_TTL_THRESHOLD,
COOLDOWN_TTL_EXTENSION,
);
let now = env.ledger().timestamp();
let elapsed = now.saturating_sub(last);
if elapsed < COOLDOWN_SECONDS {
Expand All @@ -54,9 +74,16 @@ pub fn require_cooldown(env: &Env, developer: &Address) -> Result<(), RegistryEr
/// Must be called after every successful registration so that subsequent
/// registrations by the same developer are subject to the cooldown window.
/// Registrations by other developers are not affected.
///
/// The marker TTL is extended on write using a lifetime at least as long as
/// the cooldown window so the entry cannot archive while it is still relevant.
pub fn update_cooldown(env: &Env, developer: &Address) {
let now = env.ledger().timestamp();
env.storage()
.persistent()
.set(&StorageKey::DeveloperCooldown(developer.clone()), &now);
let key = StorageKey::DeveloperCooldown(developer.clone());
env.storage().persistent().set(&key, &ow);
env.storage().persistent().extend_ttl(
@key,
COOLDOWN_TTL_THRESHOLD,
COOLDOWN_TTL_EXTENSION,
);
}
38 changes: 28 additions & 10 deletions contracts/registry/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -144,22 +144,20 @@ impl CalloraRegistry {

/// Shared registration core used by both public entrypoints.
///
/// Validates inputs, checks for duplicates, publishes to the catalog, then
/// atomically writes the offering record, increments the registered count,
/// emits the registration event, and records the developer cooldown timestamp.
/// Checks for duplicates, publishes to the catalog, then atomically writes
/// the offering record, increments the registered count, emits the
/// registration event, and records the developer cooldown timestamp.
///
/// Callers are responsible for authentication, admin-equality checks, the
/// cooldown gate, and any pre-conditions specific to their variant (e.g.
/// the developer balance gate in `register_offering_with_gate`).
/// Callers are responsible for authentication, admin-equality checks, input
/// validation, the cooldown gate, and any pre-conditions specific to their
/// variant (e.g. the developer balance gate in
/// `register_offering_with_gate`).
fn do_register(
env: &Env,
developer: Address,
offering_id: String,
metadata: String,
) -> Result<(), RegistryError> {
Self::validate_offering_id(&offering_id)?;
Self::validate_metadata(&metadata)?;

let key = StorageKey::Offering(offering_id.clone());
if env.storage().persistent().has(&key) {
return Err(RegistryError::OfferingAlreadyRegistered);
Expand Down Expand Up @@ -202,6 +200,11 @@ impl CalloraRegistry {
///
/// Cross-contract interactions happen before any registry persistent write,
/// so a reverting or panicking catalog leaves registry storage unchanged.
///
/// Error precedence: authentication, then admin equality, then input
/// validation (`offering_id` and `metadata`), then the per-developer
/// cooldown, then the duplicate check. No state is written on any failure
/// path.
pub fn register_offering(
env: Env,
caller: Address,
Expand All @@ -215,6 +218,8 @@ impl CalloraRegistry {
if caller != admin {
return Err(RegistryError::Unauthorized);
}
Self::validate_offering_id(&offering_id)?;
Self::validate_metadata(&metadata)?;
admin::require_cooldown(&env, &developer)?;
Self::do_register(&env, developer, offering_id, metadata)
}
Expand All @@ -224,6 +229,11 @@ impl CalloraRegistry {
///
/// The balance gate runs before the shared registration logic so that an
/// insufficient balance is rejected before any catalog call or state write.
///
/// Error precedence: authentication, then admin equality, then input
/// validation (`offering_id` and `metadata`), then the per-developer
/// cooldown, then the balance gate, then the duplicate check. No state is
/// written on any failure path.
pub fn register_offering_with_gate(
env: Env,
caller: Address,
Expand All @@ -239,6 +249,8 @@ impl CalloraRegistry {
if caller != admin {
return Err(RegistryError::Unauthorized);
}
Self::validate_offering_id(&offering_id)?;
Self::validate_metadata(&metadata)?;
admin::require_cooldown(&env, &developer)?;

// Balance gate: reject before any catalog interaction or state write.
Expand All @@ -252,9 +264,15 @@ impl CalloraRegistry {
}

/// Return whether `offering_id` has been registered.
///
/// This is a total predicate: a malformed `offering_id` can never be
/// registered, so it returns `Ok(false)` rather than an error. Use
/// [`Self::get_offering`] when the caller needs `InvalidOfferingId`.
pub fn is_offering_registered(env: Env, offering_id: String) -> Result<bool, RegistryError> {
Self::extend_instance_ttl(&env);
Self::validate_offering_id(&offering_id)?;
if Self::validate_offering_id(&offering_id).is_err() {
return Ok(false);
}
if Self::admin(&env).is_err() {
return Err(RegistryError::NotInitialized);
}
Expand Down
9 changes: 0 additions & 9 deletions contracts/registry/tests/metadata_validation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -458,7 +458,6 @@ fn rejected_metadata_leaves_no_partial_state_gated() {
// ---------------------------------------------------------------------------

#[test]
#[ignore = "stale test: fails against current contract; see docs/KNOWN_ISSUES.md"]
fn rejects_control_character_in_offering_id() {
let env = Env::default();
let (admin, client, developer) = setup_registry(&env);
Expand All @@ -476,7 +475,6 @@ fn rejects_control_character_in_offering_id() {
}

#[test]
#[ignore = "stale test: fails against current contract; see docs/KNOWN_ISSUES.md"]
fn rejects_line_break_in_offering_id() {
let env = Env::default();
let (admin, client, developer) = setup_registry(&env);
Expand All @@ -489,7 +487,6 @@ fn rejects_line_break_in_offering_id() {
}

#[test]
#[ignore = "stale test: fails against current contract; see docs/KNOWN_ISSUES.md"]
fn rejects_non_ascii_multibyte_offering_id() {
let env = Env::default();
let (admin, client, developer) = setup_registry(&env);
Expand All @@ -502,7 +499,6 @@ fn rejects_non_ascii_multibyte_offering_id() {
}

#[test]
#[ignore = "stale test: fails against current contract; see docs/KNOWN_ISSUES.md"]
fn rejects_leading_whitespace_in_offering_id() {
let env = Env::default();
let (admin, client, developer) = setup_registry(&env);
Expand All @@ -515,7 +511,6 @@ fn rejects_leading_whitespace_in_offering_id() {
}

#[test]
#[ignore = "stale test: fails against current contract; see docs/KNOWN_ISSUES.md"]
fn rejects_trailing_whitespace_in_offering_id() {
let env = Env::default();
let (admin, client, developer) = setup_registry(&env);
Expand All @@ -528,7 +523,6 @@ fn rejects_trailing_whitespace_in_offering_id() {
}

#[test]
#[ignore = "stale test: fails against current contract; see docs/KNOWN_ISSUES.md"]
fn rejects_whitespace_only_offering_id() {
let env = Env::default();
let (admin, client, developer) = setup_registry(&env);
Expand All @@ -541,7 +535,6 @@ fn rejects_whitespace_only_offering_id() {
}

#[test]
#[ignore = "stale test: fails against current contract; see docs/KNOWN_ISSUES.md"]
fn rejects_empty_offering_id() {
let env = Env::default();
let (admin, client, developer) = setup_registry(&env);
Expand All @@ -554,7 +547,6 @@ fn rejects_empty_offering_id() {
}

#[test]
#[ignore = "stale test: fails against current contract; see docs/KNOWN_ISSUES.md"]
fn rejects_over_length_offering_id() {
let env = Env::default();
let (admin, client, developer) = setup_registry(&env);
Expand All @@ -568,7 +560,6 @@ fn rejects_over_length_offering_id() {
}

#[test]
#[ignore = "stale test: fails against current contract; see docs/KNOWN_ISSUES.md"]
fn rejects_invalid_offering_id_in_with_gate_variant() {
let env = Env::default();
let (admin, client, developer) = setup_registry(&env);
Expand Down
1 change: 0 additions & 1 deletion contracts/stake/tests/auth_snap.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
#![cfg(test)]

extern crate std;

use callora_stake::{CalloraStake, CalloraStakeClient, SUPPORTED_CAPABILITIES};
Expand Down
24 changes: 24 additions & 0 deletions public/fonts/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Fonts Directory

This directory contains custom fonts for the Blockchain Explorer application.

## Required Font Files

The application expects the following font files:

1. **BlockchainFont-Regular.woff2** and **BlockchainFont-Regular.woff**
- Regular weight font for the main UI

2. **BlockchainFont-Bold.woff2** and **BlockchainFont-Bold.woff**
- Bold weight font for headings

3. **TechMono-Regular.woff2** and **TechMono-Regular.woff**
- Monospace font for code and hash displays

## Note

If you don't have custom fonts, the application will fall back to system fonts:
- BlockchainFont β†’ system sans-serif fonts
- TechMono β†’ system monospace fonts (Courier New, etc.)

The fonts are referenced in `public/index.html` and will be loaded automatically when available.
Binary file added public/fonts/fa-brands-400.eot
Binary file not shown.
Loading