@@ -10,12 +10,20 @@ import { DebugLogQueryTemplates, MssqlDebugConfig } from '../dto/debug.config';
1010import { LogQueryDto , LogQueryResult } from '../dto/log-query.dto' ;
1111import { SqlQueryValidator } from './sql-query-validator' ;
1212import { SwapDto , SwapStatsQueryDto , SwapStatsResponseDto , SwapStatusFilter , SwapType } from '../dto/swap-stats.dto' ;
13+ import { getChainIdForSymbol } from '../constants/chain-ids' ;
14+
15+ interface PonderLockup {
16+ preimage_hash : string ;
17+ chain_id : number ;
18+ claim_tx_hash : string | null ;
19+ }
1320
1421@Injectable ( )
1522export class SupportService implements OnModuleDestroy {
1623 private readonly logger = new LightningLogger ( SupportService ) ;
1724 private readonly sqlValidator = new SqlQueryValidator ( ) ;
1825 private boltzPool : Pool | null = null ;
26+ private ponderPool : Pool | null = null ;
1927
2028 constructor (
2129 private readonly dataSource : DataSource ,
@@ -26,6 +34,9 @@ export class SupportService implements OnModuleDestroy {
2634 if ( this . boltzPool ) {
2735 await this . boltzPool . end ( ) ;
2836 }
37+ if ( this . ponderPool ) {
38+ await this . ponderPool . end ( ) ;
39+ }
2940 }
3041
3142 private getBoltzPool ( ) : Pool {
@@ -48,6 +59,27 @@ export class SupportService implements OnModuleDestroy {
4859 return this . boltzPool ;
4960 }
5061
62+ private getPonderPool ( ) : Pool | null {
63+ if ( ! this . ponderPool ) {
64+ const pgConfig = Config . ponderPostgres ;
65+ if ( ! pgConfig . host || ! pgConfig . database ) {
66+ // Ponder is optional - return null if not configured
67+ return null ;
68+ }
69+ this . ponderPool = new Pool ( {
70+ host : pgConfig . host ,
71+ port : pgConfig . port ,
72+ database : pgConfig . database ,
73+ user : pgConfig . user ,
74+ password : pgConfig . password ,
75+ max : 5 ,
76+ idleTimeoutMillis : 30000 ,
77+ connectionTimeoutMillis : 10000 ,
78+ } ) ;
79+ }
80+ return this . ponderPool ;
81+ }
82+
5183 async getRawData ( query : DbQueryDto ) : Promise < any > {
5284 const request = this . dataSource
5385 . createQueryBuilder ( )
@@ -199,6 +231,48 @@ export class SupportService implements OnModuleDestroy {
199231 return str . charAt ( 0 ) . toLowerCase ( ) + str . slice ( 1 ) . split ( '_' ) . join ( '.' ) ;
200232 }
201233
234+ /**
235+ * Fetches claim TX hashes with chain IDs from Ponder-Claim database.
236+ * Returns a map of preimageHash -> array of { chainId, claimTxHash }
237+ *
238+ * Note: Ponder generates PostgreSQL tables with snake_case column names.
239+ */
240+ private async fetchClaimTxsFromPonder (
241+ preimageHashes : string [ ] ,
242+ ) : Promise < Map < string , Array < { chainId : number ; claimTxHash : string } > > > {
243+ const result = new Map < string , Array < { chainId : number ; claimTxHash : string } > > ( ) ;
244+
245+ if ( preimageHashes . length === 0 ) {
246+ return result ;
247+ }
248+
249+ const ponderPool = this . getPonderPool ( ) ;
250+ if ( ! ponderPool ) {
251+ return result ;
252+ }
253+
254+ try {
255+ const { rows } = await ponderPool . query < PonderLockup > (
256+ `SELECT preimage_hash, chain_id, claim_tx_hash
257+ FROM lockups
258+ WHERE preimage_hash = ANY($1) AND claim_tx_hash IS NOT NULL` ,
259+ [ preimageHashes ] ,
260+ ) ;
261+
262+ for ( const row of rows ) {
263+ if ( ! row . claim_tx_hash ) continue ;
264+
265+ const existing = result . get ( row . preimage_hash ) || [ ] ;
266+ existing . push ( { chainId : row . chain_id , claimTxHash : row . claim_tx_hash } ) ;
267+ result . set ( row . preimage_hash , existing ) ;
268+ }
269+ } catch ( e ) {
270+ this . logger . warn ( `Failed to fetch claim TXs from Ponder: ${ e . message } ` ) ;
271+ }
272+
273+ return result ;
274+ }
275+
202276 // *** PUBLIC SWAP STATS *** //
203277
204278 async getSwapStats ( query : SwapStatsQueryDto ) : Promise < SwapStatsResponseDto > {
@@ -256,9 +330,10 @@ export class SupportService implements OnModuleDestroy {
256330 }
257331
258332 private async fetchChainSwaps ( pool : Pool , query : SwapStatsQueryDto ) : Promise < SwapDto [ ] > {
259- // Fetch chain swaps with their data
333+ // Fetch chain swaps with their data (including preimageHash and preimage for claim TX lookup)
260334 const chainSwapsResult = await pool . query ( `
261- SELECT cs.*,
335+ SELECT cs.id, cs.pair, cs."orderSide", cs.status, cs."failureReason", cs.fee,
336+ cs.referral, cs."createdAt", cs."updatedAt", cs."preimageHash", cs.preimage, cs.version,
262337 sd_base.symbol as base_symbol, sd_base."lockupAddress" as base_lockup,
263338 sd_base."claimAddress" as base_claim, sd_base."expectedAmount" as base_expected,
264339 sd_base.amount as base_amount, sd_base."transactionId" as base_tx,
@@ -274,8 +349,18 @@ export class SupportService implements OnModuleDestroy {
274349 LIMIT 1000
275350 ` ) ;
276351
352+ // Extract preimageHashes for claim TX lookup
353+ // Boltz stores without 0x prefix, Ponder stores with 0x prefix
354+ const preimageHashes = chainSwapsResult . rows
355+ . map ( ( row ) => row . preimageHash as string )
356+ . filter ( ( hash ) : hash is string => ! ! hash )
357+ . map ( ( hash ) => ( hash . startsWith ( '0x' ) ? hash : `0x${ hash } ` ) ) ;
358+
359+ // Fetch claim TXs from Ponder-Claim DB
360+ const claimTxMap = await this . fetchClaimTxsFromPonder ( preimageHashes ) ;
361+
277362 return chainSwapsResult . rows
278- . map ( ( row ) => this . mapChainSwap ( row ) )
363+ . map ( ( row ) => this . mapChainSwap ( row , claimTxMap ) )
279364 . filter ( ( swap ) => this . matchesFilter ( swap , query ) ) ;
280365 }
281366
@@ -307,12 +392,42 @@ export class SupportService implements OnModuleDestroy {
307392 . filter ( ( swap ) => this . matchesFilter ( swap , query ) ) ;
308393 }
309394
310- private mapChainSwap ( row : Record < string , unknown > ) : SwapDto {
395+ private mapChainSwap (
396+ row : Record < string , unknown > ,
397+ claimTxMap ?: Map < string , Array < { chainId : number ; claimTxHash : string } > > ,
398+ ) : SwapDto {
311399 const pair = row . pair as string ;
312400 const [ base , quote ] = pair . split ( '/' ) ;
313401 const orderSide = row . orderSide as number ;
314402 const direction = orderSide === 1 ? `${ base } -> ${ quote } ` : `${ quote } -> ${ base } ` ;
315403
404+ // Determine source and destination symbols
405+ const sourceSymbol = orderSide === 1 ? base : quote ;
406+ const destSymbol = orderSide === 1 ? quote : base ;
407+
408+ // Get chain IDs for source and destination
409+ const sourceChainId = getChainIdForSymbol ( sourceSymbol ) ;
410+ const destChainId = getChainIdForSymbol ( destSymbol ) ;
411+
412+ // Look up claim TXs from Ponder data
413+ let sourceClaimTxId : string | undefined ;
414+ let destClaimTxId : string | undefined ;
415+
416+ const preimageHash = row . preimageHash as string ;
417+ if ( preimageHash && claimTxMap ) {
418+ // Normalize to 0x prefix for lookup (Ponder stores with 0x, Boltz without)
419+ const normalizedHash = preimageHash . startsWith ( '0x' ) ? preimageHash : `0x${ preimageHash } ` ;
420+ const claimTxs = claimTxMap . get ( normalizedHash ) || [ ] ;
421+
422+ for ( const claimTx of claimTxs ) {
423+ if ( sourceChainId && claimTx . chainId === sourceChainId ) {
424+ sourceClaimTxId = claimTx . claimTxHash ;
425+ } else if ( destChainId && claimTx . chainId === destChainId ) {
426+ destClaimTxId = claimTx . claimTxHash ;
427+ }
428+ }
429+ }
430+
316431 return {
317432 type : 'Chain Swap' ,
318433 id : row . id as string ,
@@ -324,16 +439,26 @@ export class SupportService implements OnModuleDestroy {
324439 referral : ( row . referral as string ) || undefined ,
325440 createdAt : this . toIsoString ( row . createdAt ) ,
326441 updatedAt : this . toIsoString ( row . updatedAt ) ,
327- sourceSymbol : orderSide === 1 ? base : quote ,
442+ // Crypto details
443+ preimageHash : preimageHash || undefined ,
444+ preimage : ( row . preimage as string ) || undefined ,
445+ version : row . version as number | undefined ,
446+ // Source chain
447+ sourceSymbol,
448+ sourceChainId,
328449 sourceAddress : ( orderSide === 1 ? row . base_lockup : row . quote_lockup ) as string ,
329450 sourceExpectedAmount : ( orderSide === 1 ? row . base_expected : row . quote_expected ) as string ,
330451 sourceAmount : ( orderSide === 1 ? row . base_amount : row . quote_amount ) as string ,
331452 sourceTxId : ( orderSide === 1 ? row . base_tx : row . quote_tx ) as string ,
332- destSymbol : orderSide === 1 ? quote : base ,
333- destAddress : ( ( orderSide === 1 ? row . quote_claim || row . quote_lockup : row . base_claim || row . base_lockup ) as string ) ,
453+ sourceClaimTxId,
454+ // Destination chain
455+ destSymbol,
456+ destChainId,
457+ destAddress : ( orderSide === 1 ? row . quote_claim || row . quote_lockup : row . base_claim || row . base_lockup ) as string ,
334458 destExpectedAmount : ( orderSide === 1 ? row . quote_expected : row . base_expected ) as string ,
335459 destAmount : ( orderSide === 1 ? row . quote_amount : row . base_amount ) as string ,
336460 destTxId : ( orderSide === 1 ? row . quote_tx : row . base_tx ) as string ,
461+ destClaimTxId,
337462 } ;
338463 }
339464
0 commit comments