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
77 changes: 28 additions & 49 deletions contracts/account/src/events.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use soroban_sdk::{contractevent, Address, Env};

#[contractevent]
pub struct AccountInitalizedEvent {
pub struct AccountInitializedEvent {
pub merchant: Address,
pub merchant_id: u64,
pub timestamp: u64,
Expand All @@ -13,110 +13,89 @@ pub fn publish_account_initialized_event(
merchant_id: u64,
timestamp: u64,
) {
AccountInitalizedEvent {
AccountInitializedEvent {
merchant,
merchant_id,
timestamp,
}
.publish(env);
}

#[contractevent]
pub struct TokenAddedEvent {
pub token: Address,
#[contractevent(topics = ["account_restricted"])]
pub struct AccountRestrictedEvent {
pub status: bool,
pub timestamp: u64,
}

pub fn publish_token_added_event(env: &Env, token: Address, timestamp: u64) {
TokenAddedEvent { token, timestamp }.publish(env);
pub fn publish_account_restricted_event(env: &Env, status: bool, timestamp: u64) {
AccountRestrictedEvent { status, timestamp }.publish(env);
}

#[contractevent]
pub struct AccountVerified {
#[contractevent(data_format = "single-value")]
pub struct AccountVerifiedEvent {
pub timestamp: u64,
}

pub fn publish_account_verified_event(env: &Env, timestamp: u64) {
AccountVerified { timestamp }.publish(env);
}

#[contractevent]
pub struct AccountRestricted {
pub status: bool,
pub timestamp: u64,
}
/// Alias used by tests.
pub type AccountVerified = AccountVerifiedEvent;

pub fn publish_account_restricted_event(env: &Env, status: bool, timestamp: u64) {
AccountRestricted { status, timestamp }.publish(env);
pub fn publish_account_verified_event(env: &Env, timestamp: u64) {
AccountVerifiedEvent { timestamp }.publish(env);
}

#[contractevent]
pub struct WithdrawalToEvent {
pub struct RefundProcessedEvent {
pub token: Address,
pub recipient: Address,
pub amount: i128,
pub recipient: Address,
pub timestamp: u64,
}

pub fn publish_withdrawal_to_event(
pub fn publish_refund_processed_event(
env: &Env,
token: Address,
recipient: Address,
amount: i128,
to: Address,
timestamp: u64,
) {
WithdrawalToEvent {
RefundProcessedEvent {
token,
recipient,
amount,
recipient: to,
timestamp,
}
.publish(env);
}

#[contractevent]
pub struct RefundProcessedEvent {
pub struct TokenAddedEvent {
pub token: Address,
pub amount: i128,
pub recipient: Address,
pub timestamp: u64,
}

pub fn publish_refund_processed_event(
env: &Env,
token: Address,
amount: i128,
recipient: Address,
timestamp: u64,
) {
RefundProcessedEvent {
token,
amount,
recipient,
timestamp,
}
.publish(env);
pub fn publish_token_added_event(env: &Env, token: Address, timestamp: u64) {
TokenAddedEvent { token, timestamp }.publish(env);
}

#[contractevent]
pub struct WithdrawalEvent {
pub struct WithdrawalToEvent {
pub token: Address,
pub amount: i128,
pub recipient: Address,
pub amount: i128,
pub timestamp: u64,
}

pub fn publish_withdrawal_event(
pub fn publish_withdrawal_to_event(
env: &Env,
token: Address,
amount: i128,
recipient: Address,
amount: i128,
timestamp: u64,
) {
WithdrawalEvent {
WithdrawalToEvent {
token,
amount,
recipient,
amount,
timestamp,
}
.publish(env);
Expand Down
131 changes: 131 additions & 0 deletions contracts/account/src/tests/test_merchant_verification.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
#![cfg(test)]

use super::*;
use soroban_sdk::{
testutils::{Address as _, Events},
vec, Address, Env, IntoVal,
};

use crate::account::MerchantAccount;
use crate::events::AccountVerified;

// ── helpers ──────────────────────────────────────────────────────────────────

fn setup() -> (Env, Address, Address, Address) {
let env = Env::default();
env.mock_all_auths();

let contract_id = env.register(MerchantAccount, ());
let client = MerchantAccountClient::new(&env, &contract_id);

let merchant = Address::generate(&env);
let manager = Address::generate(&env);

client.initialize(&merchant, &manager, &1u64);

(env, contract_id, merchant, manager)
}

// ── tests ─────────────────────────────────────────────────────────────────────

/// A newly initialized account must not be verified.
#[test]
fn test_initial_state_is_unverified() {
let (env, contract_id, _, _) = setup();
let client = MerchantAccountClient::new(&env, &contract_id);

assert!(
!client.is_verified_account(),
"account should be unverified after initialization"
);
}

/// The manager can verify an account; afterwards `is_verified_account` returns true
/// and an `AccountVerified` event is emitted.
#[test]
fn test_successful_verification() {
let (env, contract_id, _, _) = setup();
let client = MerchantAccountClient::new(&env, &contract_id);

client.verify_account();

// Status check
assert!(
client.is_verified_account(),
"account should be verified after verify_account"
);

// Event check — find the AccountVerified event in the emitted list
let events = env.events().all();
let verified_event = events.iter().find(|(contract, _topics, _data)| {
*contract == contract_id
});

assert!(
verified_event.is_some(),
"AccountVerified event should have been emitted"
);

// Confirm the event data carries a timestamp (u64)
let (_, _topics, data) = verified_event.unwrap();
let _: u64 = data.into_val(&env); // will panic if type doesn't match
}

/// Calling `verify_account` from a non-manager address must panic.
#[test]
#[should_panic]
fn test_unauthorized_verification_panics() {
let env = Env::default();
// Do NOT mock_all_auths — we want real auth enforcement
let contract_id = env.register(MerchantAccount, ());
let client = MerchantAccountClient::new(&env, &contract_id);

let merchant = Address::generate(&env);
let manager = Address::generate(&env);

// Initialize with mock auths just for setup
env.mock_all_auths();
client.initialize(&merchant, &manager, &1u64);

// Clear mocked auths so the next call uses real auth
// (creating a fresh env simulates this cleanly)
let env2 = Env::default();
let contract_id2 = env2.register(MerchantAccount, ());
let client2 = MerchantAccountClient::new(&env2, &contract_id2);

let merchant2 = Address::generate(&env2);
let manager2 = Address::generate(&env2);
let impostor = Address::generate(&env2);

env2.mock_all_auths();
client2.initialize(&merchant2, &manager2, &2u64);

// Only authorize the impostor, not the manager
env2.mock_auths(&[soroban_sdk::testutils::MockAuth {
address: &impostor,
invoke: &soroban_sdk::testutils::MockAuthInvoke {
contract: &contract_id2,
fn_name: "verify_account",
args: soroban_sdk::vec![&env2].into(),
sub_invokes: &[],
},
}]);

// This should panic because impostor != manager
client2.verify_account();
}

/// Calling `verify_account` twice must not panic — the operation is idempotent.
#[test]
fn test_verify_account_is_idempotent() {
let (env, contract_id, _, _) = setup();
let client = MerchantAccountClient::new(&env, &contract_id);

client.verify_account();
client.verify_account(); // second call must not panic

assert!(
client.is_verified_account(),
"account should still be verified after second call"
);
}
2 changes: 1 addition & 1 deletion contracts/shade/src/tests/test_account_factory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

use crate::shade::{Shade, ShadeClient};
use soroban_sdk::testutils::{Address as _, Events as _};
use soroban_sdk::{Address, BytesN, Env, Map, Symbol, TryIntoVal, Val, Vec};
use soroban_sdk::{Address, Env, Map, Symbol, TryIntoVal, Val};

// const ACCOUNT_WASM: &[u8] =
// include_bytes!("../../../../target/wasm32-unknown-unknown/release/account.wasm");
Expand Down