@@ -22,6 +22,11 @@ export function decodeU64(val: xdr.ScVal): bigint {
2222 return BigInt ( val . u64 ( ) . toString ( ) ) ;
2323}
2424
25+ /** Decode an ScVal U32 to a JavaScript number. */
26+ export function decodeU32 ( val : xdr . ScVal ) : number {
27+ return val . u32 ( ) ;
28+ }
29+
2530/**
2631 * Decode an ScVal I128 to a decimal string suitable for DB storage.
2732 * I128 in XDR is split into hi (signed Int64) and lo (unsigned Uint64).
@@ -141,6 +146,32 @@ export class SorobanEventWorker {
141146 this . pollTimer = setTimeout ( ( ) => this . poll ( ) , this . pollIntervalMs ) ;
142147 }
143148
149+ private async ensureSystemStream ( tx : any ) : Promise < void > {
150+ const systemUser = 'GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF' ;
151+ await tx . user . upsert ( {
152+ where : { publicKey : systemUser } ,
153+ create : { publicKey : systemUser } ,
154+ update : { } ,
155+ } ) ;
156+ await tx . stream . upsert ( {
157+ where : { streamId : 0 } ,
158+ create : {
159+ streamId : 0 ,
160+ sender : systemUser ,
161+ recipient : systemUser ,
162+ tokenAddress : 'CDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHF' ,
163+ ratePerSecond : '0' ,
164+ depositedAmount : '0' ,
165+ withdrawnAmount : '0' ,
166+ startTime : 0 ,
167+ lastUpdateTime : 0 ,
168+ endTime : 0 ,
169+ isActive : false ,
170+ } ,
171+ update : { } ,
172+ } ) ;
173+ }
174+
144175 private async poll ( ) : Promise < void > {
145176 this . activeBatch = this . fetchAndProcessEvents ( ) . catch ( ( err ) => {
146177 logger . error ( '[SorobanWorker] Unhandled error during poll:' , err ) ;
@@ -247,14 +278,26 @@ export class SorobanEventWorker {
247278 public async processEvent (
248279 event : rpc . Api . EventResponse ,
249280 ) : Promise < void > {
250- if ( ! event . topic || event . topic . length < 2 ) return ;
281+ if ( ! event . topic || event . topic . length < 1 ) return ;
251282
252283 const topic0 : xdr . ScVal | undefined = event . topic [ 0 ] ;
253- const topic1 : xdr . ScVal | undefined = event . topic [ 1 ] ;
254- if ( ! topic0 || ! topic1 ) return ;
284+ if ( ! topic0 ) return ;
255285
256286 const eventName = decodeSymbol ( topic0 ) ;
257287
288+ if ( eventName === 'fee_config_updated' || eventName === 'admin_transferred' ) {
289+ if ( eventName === 'fee_config_updated' ) {
290+ await this . handleFeeConfigUpdated ( event ) ;
291+ } else {
292+ await this . handleAdminTransferred ( event ) ;
293+ }
294+ return ;
295+ }
296+
297+ if ( event . topic . length < 2 ) return ;
298+ const topic1 : xdr . ScVal | undefined = event . topic [ 1 ] ;
299+ if ( ! topic1 ) return ;
300+
258301 switch ( eventName ) {
259302 case 'stream_created' :
260303 await this . handleStreamCreated ( event , topic1 ) ;
@@ -286,6 +329,106 @@ export class SorobanEventWorker {
286329 }
287330 }
288331
332+ private async handleFeeConfigUpdated (
333+ event : rpc . Api . EventResponse ,
334+ ) : Promise < void > {
335+ const body = decodeMap ( event . value ) ;
336+
337+ if (
338+ ! body [ 'admin' ] ||
339+ ! body [ 'old_treasury' ] ||
340+ ! body [ 'new_treasury' ] ||
341+ body [ 'old_fee_rate_bps' ] === undefined ||
342+ body [ 'new_fee_rate_bps' ] === undefined
343+ ) {
344+ throw new Error ( 'FeeConfigUpdated: missing body fields' ) ;
345+ }
346+
347+ const admin = decodeAddress ( body [ 'admin' ] ) ;
348+ const oldTreasury = decodeAddress ( body [ 'old_treasury' ] ) ;
349+ const newTreasury = decodeAddress ( body [ 'new_treasury' ] ) ;
350+ const oldFeeRateBps = decodeU32 ( body [ 'old_fee_rate_bps' ] ) ;
351+ const newFeeRateBps = decodeU32 ( body [ 'new_fee_rate_bps' ] ) ;
352+ const timestamp = Math . floor ( Date . now ( ) / 1000 ) ;
353+
354+ await prisma . $transaction ( async ( tx : any ) => {
355+ await this . ensureSystemStream ( tx ) ;
356+
357+ await tx . streamEvent . upsert ( {
358+ where : { transactionHash_eventType : { transactionHash : event . txHash , eventType : 'FEE_CONFIG_UPDATED' } } ,
359+ create : {
360+ streamId : 0 ,
361+ eventType : 'FEE_CONFIG_UPDATED' ,
362+ transactionHash : event . txHash ,
363+ ledgerSequence : event . ledger ,
364+ timestamp,
365+ metadata : JSON . stringify ( {
366+ admin,
367+ old_treasury : oldTreasury ,
368+ new_treasury : newTreasury ,
369+ old_fee_rate_bps : oldFeeRateBps ,
370+ new_fee_rate_bps : newFeeRateBps ,
371+ } ) ,
372+ } ,
373+ update : { } ,
374+ } ) ;
375+ } ) ;
376+
377+ sseService . broadcastToAdmin ( 'stream.fee_config_updated' , {
378+ admin,
379+ oldTreasury,
380+ newTreasury,
381+ oldFeeRateBps,
382+ newFeeRateBps,
383+ transactionHash : event . txHash ,
384+ ledger : event . ledger ,
385+ timestamp,
386+ } ) ;
387+ }
388+
389+ private async handleAdminTransferred (
390+ event : rpc . Api . EventResponse ,
391+ ) : Promise < void > {
392+ const body = decodeMap ( event . value ) ;
393+
394+ if ( ! body [ 'previous_admin' ] || ! body [ 'new_admin' ] ) {
395+ throw new Error ( 'AdminTransferred: missing body fields' ) ;
396+ }
397+
398+ const previousAdmin = decodeAddress ( body [ 'previous_admin' ] ) ;
399+ const newAdmin = decodeAddress ( body [ 'new_admin' ] ) ;
400+ const timestamp = Math . floor ( Date . now ( ) / 1000 ) ;
401+
402+ await prisma . $transaction ( async ( tx : any ) => {
403+ await this . ensureSystemStream ( tx ) ;
404+
405+ await tx . streamEvent . upsert ( {
406+ where : { transactionHash_eventType : { transactionHash : event . txHash , eventType : 'ADMIN_TRANSFERRED' } } ,
407+ create : {
408+ streamId : 0 ,
409+ eventType : 'ADMIN_TRANSFERRED' ,
410+ transactionHash : event . txHash ,
411+ ledgerSequence : event . ledger ,
412+ timestamp,
413+ metadata : JSON . stringify ( {
414+ previous_admin : previousAdmin ,
415+ new_admin : newAdmin ,
416+ transactionHash : event . txHash ,
417+ } ) ,
418+ } ,
419+ update : { } ,
420+ } ) ;
421+ } ) ;
422+
423+ sseService . broadcastToAdmin ( 'stream.admin_transferred' , {
424+ previousAdmin,
425+ newAdmin,
426+ transactionHash : event . txHash ,
427+ ledger : event . ledger ,
428+ timestamp,
429+ } ) ;
430+ }
431+
289432 // ─── Event Handlers ────────────────────────────────────────────────────────
290433
291434 private async handleStreamCreated (
0 commit comments