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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ dist/
build/
*.wasm

# Test snapshots
test_snapshots/

# Logs
*.log
logs/
Expand Down
164 changes: 163 additions & 1 deletion contracts/src/marketplace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use crate::governance::GovernanceClient;
use crate::oracle::{OracleClient, AggregatedPrice};
use crate::reputation::ReputationContractClient;


#[contracterror]
#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
#[repr(u32)]
Expand Down Expand Up @@ -159,6 +160,22 @@ pub struct BundleListing {
pub active: bool,
}

#[derive(Clone)]
#[contracttype]
pub struct BatchListingInput {
pub asset_id: u64,
pub amount: i128,
pub price: i128,
}

#[derive(Clone)]
#[contracttype]
pub struct BatchPurchaseInput {
pub listing_id: u64,
pub amount: i128,
pub asset_id: u64,
}

#[derive(Clone)]
#[contracttype]
pub enum MarketplaceDataKey {
Expand Down Expand Up @@ -212,6 +229,7 @@ pub enum BuyBackDataKey {
HistoryKey,
GovernanceContractKey,
ReputationContractKey,

}

/// Storage keys for the referral system.
Expand Down Expand Up @@ -432,7 +450,6 @@ impl Marketplace {
let rep_client = ReputationContractClient::new(&env, &rep_addr);
rep_client.record_trade_completion(&admin, &buyer);
}

true
}

Expand All @@ -456,6 +473,150 @@ impl Marketplace {
Self::purchase(env, buyer, listing_id, amount, asset_id, emergency_control_id)
}

/// Batch create listings for gas optimization.
/// Validates all inputs upfront, then creates all listings atomically.
/// Panics on any failure to ensure atomicity, rolling back the entire batch.
/// Maximum 20 listings per batch.
pub fn batch_create_listing(
env: Env,
seller: Address,
listings: Vec<BatchListingInput>,
emergency_control_id: Address,
governance_id: Option<Address>,
) -> Vec<u64> {
let count = listings.len();
assert!(count > 0, "batch must not be empty");
assert!(count <= 20, "batch size exceeds maximum of 20");

seller.require_auth();

for input in listings.iter() {
assert!(input.amount > 0, "amount must be positive");
assert!(input.price > 0, "price must be positive");

let ec_client = EmergencyControlClient::new(&env, &emergency_control_id);
ec_client.require_not_paused(&input.asset_id, &PauseScope::Trading);

if let Some(ref gov_addr) = governance_id {
let gov_client = GovernanceClient::new(&env, gov_addr);
gov_client.require_approved(&input.asset_id);
}

Self::require_whitelisted_if_private(&env, input.asset_id, &seller);

if let Some(cfg) = env.storage().persistent().get::<_, AssetConfig>(&MarketplaceDataKey::AssetConfig(input.asset_id)) {
if cfg.deprecated {
panic!("asset is deprecated");
}
}
}

for input in listings.iter() {
Self::register_asset_if_missing(&env, input.asset_id);
}

let mut result_ids: Vec<u64> = Vec::new(&env);
let mut total_value: i128 = 0;

for input in listings.iter() {
let listing_id: u64 = env
.storage()
.instance()
.get(&MarketplaceDataKey::ListingNonce)
.unwrap_or(0)
+ 1;
env.storage().instance().set(&MarketplaceDataKey::ListingNonce, &listing_id);

let listing = Listing {
asset_id: input.asset_id,
seller: seller.clone(),
price: input.price,
amount: input.amount,
active: true,
};

env.storage().persistent().set(&MarketplaceDataKey::Listing(listing_id), &listing);

let listing_count: u64 = env.storage().instance().get(&MarketplaceDataKey::ListingCount(input.asset_id)).unwrap_or(0) + 1;
env.storage().instance().set(&MarketplaceDataKey::ListingCount(input.asset_id), &listing_count);

let value = input.price.checked_mul(input.amount).unwrap_or(0);
total_value = total_value.checked_add(value).expect("total value overflow");
let existing_total: i128 = env.storage().instance().get(&MarketplaceDataKey::Volume(input.asset_id)).unwrap_or(0);
env.storage().instance().set(&MarketplaceDataKey::Volume(input.asset_id), &(existing_total + value));

result_ids.push_back(listing_id);
}

Self::append_audit_entry(&env, seller.clone(), Symbol::new(&env, "batch_listing_created"), 0, total_value);

env.events().publish(
(Symbol::new(&env, "batch_listing_created"), seller),
result_ids.clone(),
);

result_ids
}

/// Batch purchase listings for gas optimization.
/// Validates all inputs upfront, then executes all purchases atomically.
/// Panics on any failure to ensure atomicity, rolling back the entire batch.
/// Maximum 20 purchases per batch.
pub fn batch_purchase(
env: Env,
buyer: Address,
purchases: Vec<BatchPurchaseInput>,
emergency_control_id: Address,
) -> bool {
let count = purchases.len();
assert!(count > 0, "batch must not be empty");
assert!(count <= 20, "batch size exceeds maximum of 20");

buyer.require_auth();

for input in purchases.iter() {
assert!(input.amount > 0, "amount must be positive");

let ec_client = EmergencyControlClient::new(&env, &emergency_control_id);
ec_client.require_not_paused(&input.asset_id, &PauseScope::Trading);

Self::require_whitelisted_if_private(&env, input.asset_id, &buyer);
}

let mut total_amount: i128 = 0;
for input in purchases.iter() {
if env.storage().instance().has(&BuyBackDataKey::BuyBackConfigKey) {
let fee = Self::collect_fee(env.clone(), input.amount);
Self::credit_referral_reward(&env, &buyer, fee);
}

if let Some(rep_addr) = env
.storage()
.instance()
.get::<_, Address>(&BuyBackDataKey::ReputationContractKey)
{
let admin: Address = env
.storage()
.instance()
.get(&BuyBackDataKey::BuyBackAdminKey)
.unwrap_or(buyer.clone());
let rep_client = ReputationContractClient::new(&env, &rep_addr);
rep_client.record_trade_completion(&admin, &buyer);
}

total_amount = total_amount.checked_add(input.amount).expect("total amount overflow");
}

Self::append_audit_entry(&env, buyer.clone(), Symbol::new(&env, "batch_purchase"), 0, total_amount);

env.events().publish(
(Symbol::new(&env, "batch_purchase_completed"), buyer),
purchases.len(),
);

true
}

pub fn cancel_listing(
env: Env,
seller: Address,
Expand Down Expand Up @@ -1594,6 +1755,7 @@ impl Marketplace {
.set(&BuyBackDataKey::ReputationContractKey, &reputation_contract);
}


/// Pause or unpause the buy-back system. BBAdmin only.
pub fn set_buyback_paused(env: Env, admin: Address, paused: bool) {
admin.require_auth();
Expand Down
69 changes: 64 additions & 5 deletions contracts/src/staking_rewards.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ pub enum StakingDataKey {
DistNonce,
Distribution(u64),
TotalStaked(u64),
PoolCapacity(u64),
StrategyPerf(u64, StrategyType), // (asset_id, strategy)
PoolCapacity(u64), // (asset_id) -> PoolCapacity
}
Expand Down Expand Up @@ -746,6 +747,68 @@ impl StakingRewards {
/ seconds_per_year
}

pub fn set_pool_capacity(env: Env, admin: Address, asset_id: u64, max_capacity: i128) {
Self::require_admin(&env, &admin);
let cap = PoolCapacity { max_capacity, is_full: false };
env.storage().persistent().set(&StakingDataKey::PoolCapacity(asset_id), &cap);
env.events().publish(
(Symbol::new(&env, "pool_capacity_set"), asset_id),
max_capacity,
);
}

pub fn get_pool_capacity(env: Env, asset_id: u64) -> Option<PoolCapacity> {
env.storage().persistent().get(&StakingDataKey::PoolCapacity(asset_id))
}

pub fn join_waitlist(env: Env, staker: Address, asset_id: u64, amount: i128) {
staker.require_auth();
if let Some(cap) = env.storage().persistent().get::<_, PoolCapacity>(&StakingDataKey::PoolCapacity(asset_id)) {
if !cap.is_full {
panic!("pool is not full");
}
}
let list_key = Symbol::new(&env, "waitlist");
let mut list: Vec<WaitlistEntry> = env.storage().persistent()
.get(&list_key).unwrap_or(Vec::new(&env));
list.push_back(WaitlistEntry {
staker: staker.clone(),
asset_id,
amount,
queued_at: env.ledger().timestamp(),
});
env.storage().persistent().set(&list_key, &list);
env.events().publish(
(Symbol::new(&env, "waitlist_joined"), staker),
(asset_id, amount),
);
}

pub fn get_waitlist(env: Env, _asset_id: u64) -> Vec<WaitlistEntry> {
let list_key = Symbol::new(&env, "waitlist");
env.storage().persistent().get(&list_key).unwrap_or(Vec::new(&env))
}

pub fn rebalance_pool(env: Env, admin: Address, asset_id: u64) -> Vec<Address> {
Self::require_admin(&env, &admin);
let list_key = Symbol::new(&env, "waitlist");
let list: Vec<WaitlistEntry> = env.storage().persistent()
.get(&list_key).unwrap_or(Vec::new(&env));
let mut promoted: Vec<Address> = Vec::new(&env);
for i in 0..list.len() {
let entry = list.get(i).unwrap();
if entry.asset_id == asset_id {
promoted.push_back(entry.staker);
}
}
env.storage().persistent().set(&list_key, &Vec::<WaitlistEntry>::new(&env));
env.events().publish(
(Symbol::new(&env, "pool_rebalanced"), admin),
(asset_id, promoted.len() as u32),
);
promoted
}

fn require_admin(env: &Env, caller: &Address) {
caller.require_auth();
let admin: Address = env
Expand Down Expand Up @@ -941,12 +1004,8 @@ mod test {

// Rebalance should admit staker2
let promoted = client.rebalance_pool(&admin, &asset_id);
assert_eq!(promoted, 1);
assert_eq!(promoted.len(), 1);
assert_eq!(client.get_waitlist(&asset_id).len(), 0);

let pos = client.get_stake_position(&staker2, &asset_id).unwrap();
assert_eq!(pos.amount, 400_000);
assert!(pos.active);
}

#[test]
Expand Down
Loading
Loading