55#![ no_std]
66
77use soroban_sdk:: {
8- contract, contractimpl, contracttype, Env , Address , String ,
9- Vec , Symbol , IntoVal , Val , TryFromVal ,
8+ contract, contractimpl, contracttype, Address , BytesN , Env , IntoVal , String , Symbol , TryFromVal ,
9+ Val , Vec ,
1010} ;
1111
1212// ════════════════════════════════════════════════════════════════
1313// DATA STRUCTURES
1414// ════════════════════════════════════════════════════════════════
1515
16+ #[ derive( Clone ) ]
17+ #[ contracttype]
18+ enum DataKey {
19+ Admin ,
20+ ContractVersion ,
21+ }
22+
1623/// Represents a single operation in a batch
1724#[ derive( Clone ) ]
1825#[ contracttype]
@@ -197,6 +204,62 @@ pub struct SubTrackrBatch;
197204
198205#[ contractimpl]
199206impl SubTrackrBatch {
207+ /// Initialize the contract with an admin.
208+ ///
209+ /// The admin is required for upgrade operations.
210+ pub fn init ( env : Env , admin : Address ) {
211+ if env. storage ( ) . instance ( ) . has ( & DataKey :: Admin ) {
212+ panic ! ( "already initialized" ) ;
213+ }
214+
215+ admin. require_auth ( ) ;
216+ env. storage ( ) . instance ( ) . set ( & DataKey :: Admin , & admin) ;
217+ env. storage ( ) . instance ( ) . set ( & DataKey :: ContractVersion , & 1u32 ) ;
218+
219+ env. events ( )
220+ . publish ( Symbol :: new ( & env, "admin_initialized" ) , admin) ;
221+ }
222+
223+ /// Upgrade the contract WASM (admin-only).
224+ ///
225+ /// Note: state is preserved because Soroban upgrades keep instance storage.
226+ /// If you need migrations, upgrade first, then call `migrate`.
227+ pub fn upgrade ( env : Env , new_wasm_hash : BytesN < 32 > ) {
228+ let admin: Address = env
229+ . storage ( )
230+ . instance ( )
231+ . get ( & DataKey :: Admin )
232+ . unwrap_or_else ( || panic ! ( "contract not initialized" ) ) ;
233+ admin. require_auth ( ) ;
234+
235+ env. events ( ) . publish (
236+ Symbol :: new ( & env, "contract_upgrade_requested" ) ,
237+ new_wasm_hash. clone ( ) ,
238+ ) ;
239+
240+ env. deployer ( ) . update_current_contract_wasm ( new_wasm_hash) ;
241+
242+ env. events ( )
243+ . publish ( Symbol :: new ( & env, "contract_upgraded" ) , admin) ;
244+ }
245+
246+ /// Post-upgrade migration hook (admin-only).
247+ pub fn migrate ( env : Env , new_version : u32 ) {
248+ let admin: Address = env
249+ . storage ( )
250+ . instance ( )
251+ . get ( & DataKey :: Admin )
252+ . unwrap_or_else ( || panic ! ( "contract not initialized" ) ) ;
253+ admin. require_auth ( ) ;
254+
255+ env. storage ( )
256+ . instance ( )
257+ . set ( & DataKey :: ContractVersion , & new_version) ;
258+
259+ env. events ( )
260+ . publish ( Symbol :: new ( & env, "contract_migrated" ) , new_version) ;
261+ }
262+
200263 /// Execute a batch of subscription operations
201264 ///
202265 /// # Arguments
@@ -225,6 +288,18 @@ impl SubTrackrBatch {
225288 for ( index, operation) in operations. iter ( ) . enumerate ( ) {
226289 let op_index = index as u32 ;
227290
291+ // Emit intent event so indexers can follow what was requested, even if it fails later.
292+ env. events ( ) . publish (
293+ ( Symbol :: new ( & env, "operation_requested" ) , batch_id, op_index) ,
294+ (
295+ user. clone ( ) ,
296+ proxy. clone ( ) ,
297+ operation. function_name . clone ( ) ,
298+ operation. required ,
299+ operation. depends_on ,
300+ ) ,
301+ ) ;
302+
228303 // CHECK: Can we execute this operation?
229304 if should_fail && atomic {
230305 // In atomic mode, stop if previous failed
@@ -236,6 +311,11 @@ impl SubTrackrBatch {
236311 } ;
237312 results. push_back ( result) ;
238313 failed_count += 1 ;
314+
315+ env. events ( ) . publish (
316+ ( Symbol :: new ( & env, "operation_failed" ) , batch_id, op_index) ,
317+ String :: from_str ( & env, "Skipped due to atomic failure" ) ,
318+ ) ;
239319 continue ;
240320 }
241321
@@ -254,6 +334,11 @@ impl SubTrackrBatch {
254334 results. push_back ( result) ;
255335 failed_count += 1 ;
256336
337+ env. events ( ) . publish (
338+ ( Symbol :: new ( & env, "operation_failed" ) , batch_id, op_index) ,
339+ String :: from_str ( & env, "Dependency failed" ) ,
340+ ) ;
341+
257342 if operation. required {
258343 should_fail = true ;
259344 }
@@ -276,11 +361,14 @@ impl SubTrackrBatch {
276361
277362 successful_count += 1 ;
278363
279- // Emit event for operation completion
364+ // Emit generic success event
280365 env. events ( ) . publish (
281- ( Symbol :: new ( & env, "operation_success" ) , batch_id) ,
282- op_index ,
366+ ( Symbol :: new ( & env, "operation_success" ) , batch_id, op_index ) ,
367+ operation . function_name . clone ( ) ,
283368 ) ;
369+
370+ // Emit domain-level events for off-chain indexers.
371+ Self :: emit_domain_event ( & env, batch_id, op_index, & user, & proxy, operation) ;
284372 }
285373
286374 // Create batch result
@@ -346,6 +434,50 @@ impl SubTrackrBatch {
346434 ( seq << 32 ) | ( timestamp & 0xFFFFFFFF )
347435 }
348436
437+ fn emit_domain_event (
438+ env : & Env ,
439+ batch_id : u64 ,
440+ op_index : u32 ,
441+ user : & Address ,
442+ proxy : & Address ,
443+ operation : & BatchOperation ,
444+ ) {
445+ let fn_name = operation. function_name . clone ( ) ;
446+
447+ // Best-effort mapping based on operation name conventions.
448+ // Indexers can also rely on `operation_requested` / `operation_success` for full coverage.
449+ let topic = if fn_name == String :: from_str ( env, "create_plan" )
450+ || fn_name == String :: from_str ( env, "plan_create" )
451+ || fn_name == String :: from_str ( env, "createPlan" )
452+ {
453+ Some ( Symbol :: new ( env, "plan_created" ) )
454+ } else if fn_name == String :: from_str ( env, "subscribe" )
455+ || fn_name == String :: from_str ( env, "start_subscription" )
456+ || fn_name == String :: from_str ( env, "subscription_start" )
457+ {
458+ Some ( Symbol :: new ( env, "subscription_started" ) )
459+ } else if fn_name == String :: from_str ( env, "process_payment" )
460+ || fn_name == String :: from_str ( env, "payment_process" )
461+ || fn_name == String :: from_str ( env, "pay" )
462+ {
463+ Some ( Symbol :: new ( env, "payment_processed" ) )
464+ } else if fn_name == String :: from_str ( env, "cancel_subscription" )
465+ || fn_name == String :: from_str ( env, "subscription_cancel" )
466+ || fn_name == String :: from_str ( env, "cancel" )
467+ {
468+ Some ( Symbol :: new ( env, "subscription_cancelled" ) )
469+ } else {
470+ None
471+ } ;
472+
473+ if let Some ( topic) = topic {
474+ env. events ( ) . publish (
475+ ( topic, batch_id, op_index) ,
476+ ( user. clone ( ) , proxy. clone ( ) , operation. params . clone ( ) ) ,
477+ ) ;
478+ }
479+ }
480+
349481 /// Get batch status
350482 pub fn get_batch_status ( env : Env , batch_id : u64 ) -> BatchStatus {
351483 let storage_key = Symbol :: new ( & env, & format ! ( "batch_status_{}" , batch_id) ) ;
@@ -450,4 +582,4 @@ mod tests {
450582
451583 assert ! ( builder. validate( ) . is_err( ) ) ;
452584 }
453- }
585+ }
0 commit comments