@@ -5,10 +5,11 @@ extern crate std;
55use super :: * ;
66use soroban_sdk:: xdr:: ToXdr ;
77use soroban_sdk:: {
8- testutils:: { storage:: Persistent as _, Address as _, Events as _, Ledger } ,
8+ testutils:: { storage:: Persistent as _, Address as _, EnvTestConfig , Events as _, Ledger } ,
99 token:: { Client as TokenClient , StellarAssetClient } ,
1010 vec, Address , Bytes , BytesN , Env , IntoVal , Symbol ,
1111} ;
12+ use std:: { format, fs, path:: PathBuf , string:: String , vec:: Vec as StdVec } ;
1213
1314// ── helpers ──────────────────────────────────────────────────────────
1415
@@ -67,6 +68,69 @@ fn setup_bridge_with_min(
6768 ( contract_id, bridge, admin, token_addr, token, token_sac)
6869}
6970
71+ struct SnapshotEvent {
72+ topics : StdVec < String > ,
73+ data : String ,
74+ }
75+
76+ fn new_snapshot_env ( ) -> Env {
77+ Env :: new_with_config ( EnvTestConfig {
78+ capture_snapshot_at_drop : false ,
79+ } )
80+ }
81+
82+ fn snapshot_path ( name : & str ) -> PathBuf {
83+ PathBuf :: from ( "test_snapshots" )
84+ . join ( "events" )
85+ . join ( format ! ( "{name}.json" ) )
86+ }
87+
88+ fn escape_json ( value : & str ) -> String {
89+ value. replace ( '\\' , "\\ \\ " ) . replace ( '"' , "\\ \" " )
90+ }
91+
92+ fn render_snapshot ( events : & [ SnapshotEvent ] ) -> String {
93+ let mut out = String :: from ( "[\n " ) ;
94+ for ( index, event) in events. iter ( ) . enumerate ( ) {
95+ out. push_str ( " {\n " ) ;
96+ out. push_str ( " \" contract\" : \" bridge\" ,\n " ) ;
97+ out. push_str ( " \" topics\" : [\n " ) ;
98+ for ( topic_index, topic) in event. topics . iter ( ) . enumerate ( ) {
99+ out. push_str ( & format ! (
100+ " \" {}\" {}\n " ,
101+ escape_json( topic) ,
102+ if topic_index + 1 == event. topics. len( ) {
103+ ""
104+ } else {
105+ ","
106+ }
107+ ) ) ;
108+ }
109+ out. push_str ( " ],\n " ) ;
110+ out. push_str ( & format ! ( " \" data\" : \" {}\" \n " , escape_json( & event. data) ) ) ;
111+ out. push_str ( " }" ) ;
112+ if index + 1 != events. len ( ) {
113+ out. push ( ',' ) ;
114+ }
115+ out. push ( '\n' ) ;
116+ }
117+ out. push ( ']' ) ;
118+ out. push ( '\n' ) ;
119+ out
120+ }
121+
122+ fn assert_event_snapshot ( name : & str , events : & [ SnapshotEvent ] ) {
123+ let path = snapshot_path ( name) ;
124+ let expected = fs:: read_to_string ( & path)
125+ . unwrap_or_else ( |err| panic ! ( "failed to read snapshot {}: {err}" , path. display( ) ) ) ;
126+ let actual = render_snapshot ( events) ;
127+ assert_eq ! (
128+ expected,
129+ actual,
130+ "snapshot mismatch for {name}. Update {} if this event change is intentional." ,
131+ path. display( )
132+ ) ;
133+ }
70134
71135// ── happy-path tests ──────────────────────────────────────────────────
72136
@@ -2811,6 +2875,295 @@ fn test_event_version_deny_add_remove() {
28112875
28122876// ── Property-based tests (proptest) ──────────────────────────────────────────
28132877
2878+ #[ test]
2879+ fn test_event_snapshot_heartbeat ( ) {
2880+ let env = new_snapshot_env ( ) ;
2881+ env. mock_all_auths ( ) ;
2882+ let ( contract_id, bridge, _, _, _, _) = setup_bridge ( & env, 1_000 ) ;
2883+ let operator = Address :: generate ( & env) ;
2884+
2885+ bridge. set_operator ( & operator, & true ) ;
2886+ env. ledger ( ) . with_mut ( |li| {
2887+ li. sequence_number = 12_345 ;
2888+ } ) ;
2889+
2890+ bridge. heartbeat ( & operator, & 0 ) ;
2891+
2892+ assert_eq ! (
2893+ env. events( ) . all( ) . filter_by_contract( & contract_id) ,
2894+ vec![
2895+ & env,
2896+ (
2897+ contract_id. clone( ) ,
2898+ vec![
2899+ & env,
2900+ EVENT_VERSION . into_val( & env) ,
2901+ Symbol :: new( & env, "nonce_inc" ) . into_val( & env) ,
2902+ operator. clone( ) . into_val( & env)
2903+ ] ,
2904+ 1u64 . into_val( & env)
2905+ ) ,
2906+ (
2907+ contract_id,
2908+ vec![
2909+ & env,
2910+ EVENT_VERSION . into_val( & env) ,
2911+ Symbol :: new( & env, "heartbeat" ) . into_val( & env) ,
2912+ operator. into_val( & env)
2913+ ] ,
2914+ 12_345u32 . into_val( & env)
2915+ )
2916+ ]
2917+ ) ;
2918+
2919+ assert_event_snapshot (
2920+ "heartbeat" ,
2921+ & [
2922+ SnapshotEvent {
2923+ topics : StdVec :: from ( [
2924+ "u32:1" . into ( ) ,
2925+ "symbol:nonce_inc" . into ( ) ,
2926+ "address:operator" . into ( ) ,
2927+ ] ) ,
2928+ data : "u64:1" . into ( ) ,
2929+ } ,
2930+ SnapshotEvent {
2931+ topics : StdVec :: from ( [
2932+ "u32:1" . into ( ) ,
2933+ "symbol:heartbeat" . into ( ) ,
2934+ "address:operator" . into ( ) ,
2935+ ] ) ,
2936+ data : "u32:12345" . into ( ) ,
2937+ } ,
2938+ ] ,
2939+ ) ;
2940+ }
2941+
2942+ #[ test]
2943+ fn test_event_snapshot_deny_add ( ) {
2944+ let env = new_snapshot_env ( ) ;
2945+ env. mock_all_auths ( ) ;
2946+ let ( contract_id, bridge, _, _, _, _) = setup_bridge ( & env, 1_000 ) ;
2947+ let target = Address :: generate ( & env) ;
2948+
2949+ bridge. deny_address ( & target) ;
2950+
2951+ assert_eq ! (
2952+ env. events( ) . all( ) . filter_by_contract( & contract_id) ,
2953+ vec![
2954+ & env,
2955+ (
2956+ contract_id,
2957+ vec![
2958+ & env,
2959+ EVENT_VERSION . into_val( & env) ,
2960+ Symbol :: new( & env, "deny_add" ) . into_val( & env)
2961+ ] ,
2962+ target. into_val( & env)
2963+ )
2964+ ]
2965+ ) ;
2966+
2967+ assert_event_snapshot (
2968+ "deny_add" ,
2969+ & [ SnapshotEvent {
2970+ topics : StdVec :: from ( [ "u32:1" . into ( ) , "symbol:deny_add" . into ( ) ] ) ,
2971+ data : "address:target" . into ( ) ,
2972+ } ] ,
2973+ ) ;
2974+ }
2975+
2976+ #[ test]
2977+ fn test_event_snapshot_deny_rem ( ) {
2978+ let env = new_snapshot_env ( ) ;
2979+ env. mock_all_auths ( ) ;
2980+ let ( contract_id, bridge, _, _, _, _) = setup_bridge ( & env, 1_000 ) ;
2981+ let target = Address :: generate ( & env) ;
2982+
2983+ env. as_contract ( & contract_id, || {
2984+ env. storage ( )
2985+ . persistent ( )
2986+ . set ( & DataKey :: Denied ( target. clone ( ) ) , & true ) ;
2987+ } ) ;
2988+
2989+ bridge. remove_denied_address ( & target) ;
2990+
2991+ assert_eq ! (
2992+ env. events( ) . all( ) . filter_by_contract( & contract_id) ,
2993+ vec![
2994+ & env,
2995+ (
2996+ contract_id,
2997+ vec![
2998+ & env,
2999+ EVENT_VERSION . into_val( & env) ,
3000+ Symbol :: new( & env, "deny_rem" ) . into_val( & env)
3001+ ] ,
3002+ target. into_val( & env)
3003+ )
3004+ ]
3005+ ) ;
3006+
3007+ assert_event_snapshot (
3008+ "deny_rem" ,
3009+ & [ SnapshotEvent {
3010+ topics : StdVec :: from ( [ "u32:1" . into ( ) , "symbol:deny_rem" . into ( ) ] ) ,
3011+ data : "address:target" . into ( ) ,
3012+ } ] ,
3013+ ) ;
3014+ }
3015+
3016+ #[ test]
3017+ fn test_event_snapshot_quota_reset ( ) {
3018+ let env = new_snapshot_env ( ) ;
3019+ env. mock_all_auths ( ) ;
3020+ let ( contract_id, bridge, admin, token_addr, _, token_sac) = setup_bridge ( & env, 10_000 ) ;
3021+ let user = Address :: generate ( & env) ;
3022+
3023+ token_sac. mint ( & user, & 5_000 ) ;
3024+ bridge. deposit ( & user, & 2_000 , & token_addr, & Bytes :: new ( & env) , & 0 , & 0 , & None ) ;
3025+ bridge. set_withdrawal_quota ( & 500 ) ;
3026+ bridge. withdraw ( & admin, & user, & 500 , & token_addr) ;
3027+
3028+ let result = bridge. try_withdraw ( & admin, & user, & 100 , & token_addr) ;
3029+ assert_eq ! ( result, Err ( Ok ( Error :: WithdrawalQuotaExceeded ) ) ) ;
3030+
3031+ let start_ledger = env. ledger ( ) . sequence ( ) ;
3032+ env. ledger ( ) . with_mut ( |li| {
3033+ li. sequence_number = start_ledger + 17_280 ;
3034+ } ) ;
3035+
3036+ bridge. withdraw ( & admin, & user, & 500 , & token_addr) ;
3037+
3038+ assert_eq ! (
3039+ env. events( ) . all( ) . filter_by_contract( & contract_id) ,
3040+ vec![
3041+ & env,
3042+ (
3043+ contract_id. clone( ) ,
3044+ vec![
3045+ & env,
3046+ EVENT_VERSION . into_val( & env) ,
3047+ Symbol :: new( & env, "quota_reset" ) . into_val( & env)
3048+ ] ,
3049+ ( user. clone( ) , start_ledger + 17_280 ) . into_val( & env)
3050+ ) ,
3051+ (
3052+ contract_id,
3053+ vec![
3054+ & env,
3055+ EVENT_VERSION . into_val( & env) ,
3056+ Symbol :: new( & env, "withdraw" ) . into_val( & env) ,
3057+ user. into_val( & env)
3058+ ] ,
3059+ 500i128 . into_val( & env)
3060+ )
3061+ ]
3062+ ) ;
3063+
3064+ assert_event_snapshot (
3065+ "quota_reset" ,
3066+ & [
3067+ SnapshotEvent {
3068+ topics : StdVec :: from ( [ "u32:1" . into ( ) , "symbol:quota_reset" . into ( ) ] ) ,
3069+ data : "tuple:[address:user,u32:17281]" . into ( ) ,
3070+ } ,
3071+ SnapshotEvent {
3072+ topics : StdVec :: from ( [
3073+ "u32:1" . into ( ) ,
3074+ "symbol:withdraw" . into ( ) ,
3075+ "address:user" . into ( ) ,
3076+ ] ) ,
3077+ data : "i128:500" . into ( ) ,
3078+ } ,
3079+ ] ,
3080+ ) ;
3081+ }
3082+
3083+ #[ test]
3084+ fn test_event_snapshot_fee_accrued ( ) {
3085+ let env = new_snapshot_env ( ) ;
3086+ env. mock_all_auths ( ) ;
3087+ let ( contract_id, bridge, _, token_addr, _, _) = setup_bridge ( & env, 1_000 ) ;
3088+
3089+ bridge. accrue_fee ( & token_addr, & 250 ) ;
3090+
3091+ assert_eq ! (
3092+ env. events( ) . all( ) . filter_by_contract( & contract_id) ,
3093+ vec![
3094+ & env,
3095+ (
3096+ contract_id,
3097+ vec![
3098+ & env,
3099+ EVENT_VERSION . into_val( & env) ,
3100+ Symbol :: new( & env, "fee_accrue" ) . into_val( & env) ,
3101+ token_addr. into_val( & env)
3102+ ] ,
3103+ 250i128 . into_val( & env)
3104+ )
3105+ ]
3106+ ) ;
3107+
3108+ assert_event_snapshot (
3109+ "fee_accrued" ,
3110+ & [ SnapshotEvent {
3111+ topics : StdVec :: from ( [
3112+ "u32:1" . into ( ) ,
3113+ "symbol:fee_accrue" . into ( ) ,
3114+ "address:token" . into ( ) ,
3115+ ] ) ,
3116+ data : "i128:250" . into ( ) ,
3117+ } ] ,
3118+ ) ;
3119+ }
3120+
3121+ #[ test]
3122+ fn test_event_snapshot_fees_withdrawn ( ) {
3123+ let env = new_snapshot_env ( ) ;
3124+ env. mock_all_auths ( ) ;
3125+ let ( contract_id, bridge, _, token_addr, _, token_sac) = setup_bridge ( & env, 1_000 ) ;
3126+ let recipient = Address :: generate ( & env) ;
3127+
3128+ token_sac. mint ( & contract_id, & 400 ) ;
3129+ env. as_contract ( & contract_id, || {
3130+ env. storage ( )
3131+ . persistent ( )
3132+ . set ( & DataKey :: FeeVault ( token_addr. clone ( ) ) , & 400i128 ) ;
3133+ } ) ;
3134+
3135+ bridge. withdraw_fees ( & recipient, & token_addr, & 150 ) ;
3136+
3137+ assert_eq ! (
3138+ env. events( ) . all( ) . filter_by_contract( & contract_id) ,
3139+ vec![
3140+ & env,
3141+ (
3142+ contract_id,
3143+ vec![
3144+ & env,
3145+ EVENT_VERSION . into_val( & env) ,
3146+ Symbol :: new( & env, "fee_wdrw" ) . into_val( & env) ,
3147+ recipient. into_val( & env)
3148+ ] ,
3149+ 150i128 . into_val( & env)
3150+ )
3151+ ]
3152+ ) ;
3153+
3154+ assert_event_snapshot (
3155+ "fees_withdrawn" ,
3156+ & [ SnapshotEvent {
3157+ topics : StdVec :: from ( [
3158+ "u32:1" . into ( ) ,
3159+ "symbol:fee_wdrw" . into ( ) ,
3160+ "address:recipient" . into ( ) ,
3161+ ] ) ,
3162+ data : "i128:150" . into ( ) ,
3163+ } ] ,
3164+ ) ;
3165+ }
3166+
28143167#[ cfg( test) ]
28153168mod proptest_deposit {
28163169 use super :: * ;
0 commit comments