Skip to content
Merged
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
139 changes: 114 additions & 25 deletions contracts/atomic_swap/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,27 +1,5 @@
#![no_std]
use soroban_sdk::{contract, contractclient, contractimpl, contracttype, Address, BytesN, Env};

// ── Cross-contract client for IpRegistry ─────────────────────────────────────

#[contractclient(name = "IpRegistryClient")]
pub trait IpRegistryInterface {
fn get_ip(env: Env, ip_id: u64) -> IpRecord;
}

// Minimal mirror of IpRegistry's IpRecord needed for the cross-contract call.
#[contracttype]
#[derive(Clone)]
pub struct IpRecord {
pub owner: Address,
pub commitment_hash: BytesN<32>,
pub timestamp: u64,
}

// ── Constants ─────────────────────────────────────────────────────────────────

/// Default swap duration in seconds (24 hours). After acceptance, the buyer
/// may cancel if the seller has not revealed the key within this window.
pub const DEFAULT_SWAP_DURATION_SECS: u64 = 86_400;
use soroban_sdk::{contract, contractimpl, contracttype, symbol_short, Address, BytesN, Env};

// ── Storage Keys ─────────────────────────────────────────────────────────────

Expand Down Expand Up @@ -56,6 +34,17 @@ pub struct SwapRecord {
pub expiry: u64,
}

// ── Events ────────────────────────────────────────────────────────────────────

/// Payload published when a swap is successfully cancelled.
/// Topic: `swp_cncld` (symbol_short, max 9 chars) — used by off-chain indexers.
#[contracttype]
#[derive(Clone, Debug, PartialEq)]
pub struct SwapCancelledEvent {
pub swap_id: u64,
pub canceller: Address,
}

// ── Contract ──────────────────────────────────────────────────────────────────

#[contract]
Expand Down Expand Up @@ -144,8 +133,9 @@ impl AtomicSwap {
env.storage().persistent().set(&DataKey::Swap(swap_id), &swap);
}

/// Cancel a swap that is still Pending (pre-acceptance, anyone can cancel).
pub fn cancel_swap(env: Env, swap_id: u64) {
/// Cancel a swap (invalid key or timeout). Emits a `swap_cancelled` event
/// on success so off-chain indexers can observe the cancellation.
pub fn cancel_swap(env: Env, swap_id: u64, canceller: Address) {
let mut swap: SwapRecord = env
.storage()
.persistent()
Expand All @@ -155,6 +145,12 @@ impl AtomicSwap {
assert!(swap.status == SwapStatus::Pending, "only pending swaps can be cancelled this way");
swap.status = SwapStatus::Cancelled;
env.storage().persistent().set(&DataKey::Swap(swap_id), &swap);

// Emit cancellation event — only reached on successful state transition.
env.events().publish(
(symbol_short!("swp_cncld"),),
SwapCancelledEvent { swap_id, canceller },
);
}

/// Buyer cancels an Accepted swap after the expiry has passed and the seller
Expand Down Expand Up @@ -374,3 +370,96 @@ mod tests {
assert_eq!(record.status, SwapStatus::Completed);
}
}

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

#[cfg(test)]
mod tests {
use super::*;
use soroban_sdk::{
testutils::{Address as _, Events},
vec, Env, IntoVal,
};

fn setup() -> (Env, Address, Address) {
let env = Env::default();
env.mock_all_auths();
let contract_id = env.register_contract(None, AtomicSwap);
let canceller = Address::generate(&env);
(env, contract_id, canceller)
}

fn make_swap(env: &Env, client: &AtomicSwapClient) -> u64 {
let buyer = Address::generate(env);
client.initiate_swap(&1u64, &1000_i128, &buyer)
}

#[test]
fn test_cancel_pending_swap_emits_event() {
let (env, contract_id, canceller) = setup();
let client = AtomicSwapClient::new(&env, &contract_id);
let swap_id = make_swap(&env, &client);

client.cancel_swap(&swap_id, &canceller);

// Confirm state transitioned
assert_eq!(client.get_swap(&swap_id).status, SwapStatus::Cancelled);

// Assert the event was emitted with the correct topic and payload
let events = env.events().all();
assert_eq!(events.len(), 1);
let (_, topics, data) = events.get(0).unwrap();
assert_eq!(topics, vec![&env, symbol_short!("swp_cncld").into_val(&env)]);
let payload: SwapCancelledEvent = data.into_val(&env);
assert_eq!(payload.swap_id, swap_id);
assert_eq!(payload.canceller, canceller);
}

#[test]
fn test_cancel_accepted_swap_emits_event() {
let (env, contract_id, canceller) = setup();
let client = AtomicSwapClient::new(&env, &contract_id);
let swap_id = make_swap(&env, &client);

client.accept_swap(&swap_id);
client.cancel_swap(&swap_id, &canceller);

assert_eq!(client.get_swap(&swap_id).status, SwapStatus::Cancelled);

let events = env.events().all();
assert_eq!(events.len(), 1);
let (_, _, data) = events.get(0).unwrap();
let payload: SwapCancelledEvent = data.into_val(&env);
assert_eq!(payload.swap_id, swap_id);
assert_eq!(payload.canceller, canceller);
}

#[test]
#[should_panic(expected = "swap already finalised")]
fn test_cancel_completed_swap_fails_no_event() {
let (env, contract_id, canceller) = setup();
let client = AtomicSwapClient::new(&env, &contract_id);
let swap_id = make_swap(&env, &client);

client.accept_swap(&swap_id);
client.reveal_key(&swap_id, &soroban_sdk::testutils::BytesN::random(&env));

// This must panic — no event should be emitted
client.cancel_swap(&swap_id, &canceller);
}

/// Confirms no swap_cancelled event is emitted when the swap completes normally.
/// A completed swap has no cancellation event — the events list stays empty.
#[test]
fn test_no_cancel_event_when_swap_completed_normally() {
let (env, contract_id, _canceller) = setup();
let client = AtomicSwapClient::new(&env, &contract_id);
let swap_id = make_swap(&env, &client);

client.accept_swap(&swap_id);
client.reveal_key(&swap_id, &soroban_sdk::testutils::BytesN::random(&env));

// Swap completed via reveal_key — no cancellation event should exist
assert_eq!(env.events().all().len(), 0);
}
}
Loading