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
16 changes: 16 additions & 0 deletions packages/core/test/errors.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
use soroban_sdk::contracterror;

#[contracterror]
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
#[repr(u32)]
pub enum AuctionError {
NotWinner = 1,
AlreadyClaimed = 2,
NotClosed = 3,
NoFactoryContract = 4,
Unauthorized = 5,
InvalidState = 6,
BidTooLow = 7,
AuctionNotOpen = 8,
AuctionNotClosed = 9,
}
106 changes: 106 additions & 0 deletions packages/core/test/events.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
use soroban_sdk::{contractevent, symbol_short, Address, BytesN, Env, Symbol};

// Event symbols must be <= 9 chars (Soroban `symbol_short!`).
pub const AUCTION_CREATED: Symbol = symbol_short!("AUCR_CRT");
pub const BID_PLACED: Symbol = symbol_short!("BID_PLCD");
pub const AUCTION_CLOSED: Symbol = symbol_short!("AUCR_CLSD");
pub const USERNAME_CLAIMED: Symbol = symbol_short!("USR_CLMD");
pub const BID_REFUNDED: Symbol = symbol_short!("BID_RFDN");

#[contractevent]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct AuctionCreatedEvent {
#[topic]
pub username_hash: BytesN<32>,
pub end_time: u64,
pub min_bid: i128,
}

#[contractevent]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct BidPlacedEvent {
#[topic]
pub username_hash: BytesN<32>,
pub bidder: Address,
pub amount: i128,
}

#[contractevent]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct AuctionClosedEvent {
#[topic]
pub username_hash: BytesN<32>,
pub winner: Option<Address>,
pub winning_bid: u128,
}

#[contractevent]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct UsernameClaimedEvent {
#[topic]
pub username_hash: BytesN<32>,
pub claimer: Address,
}

#[contractevent]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct BidRefundedEvent {
#[topic]
pub username_hash: BytesN<32>,
pub bidder: Address,
pub refund_amount: i128,
}

pub fn emit_auction_created(env: &Env, username_hash: &BytesN<32>, end_time: u64, min_bid: i128) {
AuctionCreatedEvent {
username_hash: username_hash.clone(),
end_time,
min_bid,
}
.publish(env);
}

pub fn emit_bid_placed(env: &Env, username_hash: &BytesN<32>, bidder: &Address, amount: i128) {
BidPlacedEvent {
username_hash: username_hash.clone(),
bidder: bidder.clone(),
amount,
}
.publish(env);
}

pub fn emit_auction_closed(
env: &Env,
username_hash: &BytesN<32>,
winner: Option<Address>,
winning_bid: u128,
) {
AuctionClosedEvent {
username_hash: username_hash.clone(),
winner,
winning_bid,
}
.publish(env);
}

pub fn emit_username_claimed(env: &Env, username_hash: &BytesN<32>, claimer: &Address) {
UsernameClaimedEvent {
username_hash: username_hash.clone(),
claimer: claimer.clone(),
}
.publish(env);
}

pub fn emit_bid_refunded(
env: &Env,
username_hash: &BytesN<32>,
bidder: &Address,
refund_amount: i128,
) {
BidRefundedEvent {
username_hash: username_hash.clone(),
bidder: bidder.clone(),
refund_amount,
}
.publish(env);
}
180 changes: 180 additions & 0 deletions packages/core/test/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
#![no_std]
use soroban_sdk::{contract, contractimpl, vec, Address, BytesN, Env, IntoVal, Symbol};

pub mod errors;
pub mod events;
pub mod storage;
pub mod types;

// Ensure event symbols are linked from the main contract entrypoint module.
use crate::events::{AUCTION_CLOSED, AUCTION_CREATED, BID_PLACED, BID_REFUNDED, USERNAME_CLAIMED};

#[allow(dead_code)]
fn _touch_event_symbols() {
let _ = (
AUCTION_CREATED,
BID_PLACED,
AUCTION_CLOSED,
USERNAME_CLAIMED,
BID_REFUNDED,
);
}

#[cfg(test)]
mod test;

#[contract]
pub struct AuctionContract;

#[contractimpl]
impl AuctionContract {
pub fn close_auction(
env: Env,
username_hash: BytesN<32>,
) -> Result<(), crate::errors::AuctionError> {
let status = storage::get_status(&env);

// Reject if status is not Open
if status != types::AuctionStatus::Open {
return Err(crate::errors::AuctionError::AuctionNotOpen);
}

// Get current ledger timestamp and end time
let current_time = env.ledger().timestamp();
let end_time = storage::get_end_time(&env);

// Reject if timestamp < end_time
if current_time < end_time {
return Err(crate::errors::AuctionError::AuctionNotClosed);
}

// Set status to Closed
storage::set_status(&env, types::AuctionStatus::Closed);

// Get winner and winning bid
let winner = storage::get_highest_bidder(&env);
let winning_bid = storage::get_highest_bid(&env);

// Emit AUCTION_CLOSED event with winner and winning bid
events::emit_auction_closed(&env, &username_hash, winner.clone(), winning_bid);

Ok(())
}

pub fn claim_username(
env: Env,
username_hash: BytesN<32>,
claimer: Address,
) -> Result<(), crate::errors::AuctionError> {
claimer.require_auth();

let status = storage::get_status(&env);

if status == types::AuctionStatus::Claimed {
return Err(crate::errors::AuctionError::AlreadyClaimed);
}

if status != types::AuctionStatus::Closed {
return Err(crate::errors::AuctionError::NotClosed);
}

let highest_bidder = storage::get_highest_bidder(&env);
if !highest_bidder.map(|h| h == claimer).unwrap_or(false) {
return Err(crate::errors::AuctionError::NotWinner);
}

// Set status to Claimed
storage::set_status(&env, types::AuctionStatus::Claimed);

// Call factory_contract.deploy_username(username_hash, claimer)
let factory = storage::get_factory_contract(&env);
if factory.is_none() {
return Err(crate::errors::AuctionError::NoFactoryContract);
}

let factory_addr = factory.ok_or(crate::errors::AuctionError::NoFactoryContract)?;
env.invoke_contract::<()>(
&factory_addr,
&Symbol::new(&env, "deploy_username"),
vec![&env, username_hash.into_val(&env), claimer.into_val(&env)],
);

// Emit USERNAME_CLAIMED event
events::emit_username_claimed(&env, &username_hash, &claimer);

Ok(())
}
}

#[contractimpl]
impl AuctionContract {
pub fn create_auction(
env: Env,
id: u32,
seller: Address,
asset: Address,
min_bid: i128,
end_time: u64,
) {
seller.require_auth();
if storage::auction_exists(&env, id) {
soroban_sdk::panic_with_error!(&env, errors::AuctionError::AuctionNotOpen);
}
storage::auction_set_seller(&env, id, &seller);
storage::auction_set_asset(&env, id, &asset);
storage::auction_set_min_bid(&env, id, min_bid);
storage::auction_set_end_time(&env, id, end_time);
storage::auction_set_status(&env, id, types::AuctionStatus::Open);
}

pub fn place_bid(env: Env, id: u32, bidder: Address, amount: i128) {
bidder.require_auth();
let end_time = storage::auction_get_end_time(&env, id);
if env.ledger().timestamp() >= end_time {
soroban_sdk::panic_with_error!(&env, errors::AuctionError::AuctionNotOpen);
}
let min_bid = storage::auction_get_min_bid(&env, id);
let highest_bid = storage::auction_get_highest_bid(&env, id);
if amount < min_bid || amount <= highest_bid {
soroban_sdk::panic_with_error!(&env, errors::AuctionError::BidTooLow);
}
let asset = storage::auction_get_asset(&env, id);
let token = soroban_sdk::token::Client::new(&env, &asset);
token.transfer(&bidder, env.current_contract_address(), &amount);
if let Some(prev_bidder) = storage::auction_get_highest_bidder(&env, id) {
token.transfer(&env.current_contract_address(), &prev_bidder, &highest_bid);
}
storage::auction_set_highest_bidder(&env, id, &bidder);
storage::auction_set_highest_bid(&env, id, amount);
}

pub fn close_auction_by_id(env: Env, id: u32) {
let end_time = storage::auction_get_end_time(&env, id);
if env.ledger().timestamp() < end_time {
soroban_sdk::panic_with_error!(&env, errors::AuctionError::AuctionNotClosed);
}
storage::auction_set_status(&env, id, types::AuctionStatus::Closed);
}

pub fn claim(env: Env, id: u32, claimant: Address) {
claimant.require_auth();
let status = storage::auction_get_status(&env, id);
if status != types::AuctionStatus::Closed {
soroban_sdk::panic_with_error!(&env, errors::AuctionError::NotClosed);
}
if storage::auction_is_claimed(&env, id) {
soroban_sdk::panic_with_error!(&env, errors::AuctionError::AlreadyClaimed);
}
let winner = storage::auction_get_highest_bidder(&env, id);
if winner.as_ref().map(|w| w == &claimant).unwrap_or(false) {
let asset = storage::auction_get_asset(&env, id);
let token = soroban_sdk::token::Client::new(&env, &asset);
let winning_bid = storage::auction_get_highest_bid(&env, id);
let seller = storage::auction_get_seller(&env, id);
token.transfer(&env.current_contract_address(), &seller, &winning_bid);
storage::auction_set_claimed(&env, id);
} else {
soroban_sdk::panic_with_error!(&env, errors::AuctionError::NotWinner);
}
}
}
Loading