@@ -324,120 +324,105 @@ interface OrdpoolStatColumn {
324324}
325325
326326const camelToSnake = ( s : string ) : string => s . replace ( / [ A - Z ] / g, m => '_' + m . toLowerCase ( ) ) ;
327-
328- // Section helpers: each takes a list of camelCase field names and emits one
329- // OrdpoolStatColumn per field with `<section>_<snake>` SQL column, the
330- // matching camelCase alias, and typed get/set lambdas. TypeScript checks
331- // the field names against the actual OrdpoolStats shape.
332- const amountCols = ( ...fields : ( keyof OrdpoolStats [ 'amounts' ] ) [ ] ) : OrdpoolStatColumn [ ] =>
333- fields . map ( f => ( {
334- col : 'amounts_' + camelToSnake ( f as string ) ,
335- alias : 'amounts' + ( f as string ) . charAt ( 0 ) . toUpperCase ( ) + ( f as string ) . slice ( 1 ) ,
336- val : s => s . amounts [ f ] ,
337- set : ( t , v ) => { ( t . amounts as any ) [ f ] = v ; } ,
338- } ) ) ;
339- const feeCols = ( ...fields : ( keyof OrdpoolStats [ 'fees' ] ) [ ] ) : OrdpoolStatColumn [ ] =>
340- fields . map ( f => ( {
341- col : 'fees_' + camelToSnake ( f as string ) ,
342- alias : 'fees' + ( f as string ) . charAt ( 0 ) . toUpperCase ( ) + ( f as string ) . slice ( 1 ) ,
343- val : s => s . fees [ f ] ,
344- set : ( t , v ) => { ( t . fees as any ) [ f ] = v ; } ,
345- } ) ) ;
346- const cat21Cols = ( ...fields : ( keyof OrdpoolStats [ 'cat21' ] ) [ ] ) : OrdpoolStatColumn [ ] =>
327+ const cap = ( s : string ) : string => s . charAt ( 0 ) . toUpperCase ( ) + s . slice ( 1 ) ;
328+
329+ const MOST_ACTIVE_MINT_TRUNC = 20 ;
330+ const TRUNCATED_PLACEHOLDER = `LEFT(?, ${ MOST_ACTIVE_MINT_TRUNC } )` ;
331+
332+ /** Emit one OrdpoolStatColumn per camelCase field, mapping
333+ * `field` ↔ `${colPrefix}_${snake_field}` (SQL) ↔ `${aliasPrefix}${PascalField}` (camelCase).
334+ * `pick` returns the section object on a stats — typically a top-level key
335+ * (`s => s.amounts`) but can drill deeper (`s => s.inscriptions.image`). */
336+ const sectionCols = < Section extends object > (
337+ colPrefix : string ,
338+ aliasPrefix : string ,
339+ pick : ( s : OrdpoolStats ) => Section ,
340+ fields : ( keyof Section & string ) [ ] ,
341+ ) : OrdpoolStatColumn [ ] =>
347342 fields . map ( f => ( {
348- col : 'cat21_' + camelToSnake ( f as string ) ,
349- alias : 'cat21' + ( f as string ) . charAt ( 0 ) . toUpperCase ( ) + ( f as string ) . slice ( 1 ) ,
350- val : s => s . cat21 [ f ] ,
351- set : ( t , v ) => { ( t . cat21 as any ) [ f ] = v ; } ,
352- } ) ) ;
353- const runeCols = ( ...fields : ( keyof OrdpoolStats [ 'runes' ] ) [ ] ) : OrdpoolStatColumn [ ] =>
354- fields . map ( f => ( {
355- col : 'runes_' + camelToSnake ( f as string ) ,
356- alias : 'runes' + ( f as string ) . charAt ( 0 ) . toUpperCase ( ) + ( f as string ) . slice ( 1 ) ,
357- val : s => s . runes [ f ] ,
358- set : ( t , v ) => { ( t . runes as any ) [ f ] = v ; } ,
343+ col : `${ colPrefix } _${ camelToSnake ( f ) } ` ,
344+ alias : aliasPrefix + cap ( f ) ,
345+ val : s => ( pick ( s ) as any ) [ f ] ,
346+ set : ( t , v ) => { ( pick ( t ) as any ) [ f ] = v ; } ,
359347 } ) ) ;
360348
361- // All 8 fields of an InscriptionSizeAggregate. Used 4× — once for the global
362- // aggregate and once per content-type bucket (image/text/json).
349+ /** All 8 fields of an InscriptionSizeAggregate. Used 4× — global + per-bucket. */
350+ const INSCRIPTION_SIZE_FIELDS : ( keyof InscriptionSizeAggregate ) [ ] = [
351+ 'totalEnvelopeSize' , 'totalContentSize' ,
352+ 'largestEnvelopeSize' , 'largestContentSize' ,
353+ 'largestEnvelopeInscriptionId' , 'largestContentInscriptionId' ,
354+ 'averageEnvelopeSize' , 'averageContentSize' ,
355+ ] ;
356+
363357const inscriptionSizeCols = (
364- colPrefix : string , // e.g. 'inscriptions_image'
365- aliasPrefix : string , // e.g. 'inscriptionsImage'
358+ colPrefix : string ,
359+ aliasPrefix : string ,
366360 pick : ( s : OrdpoolStats ) => InscriptionSizeAggregate ,
367- pickTarget : ( t : OrdpoolStats ) => InscriptionSizeAggregate ,
368- ) : OrdpoolStatColumn [ ] => {
369- const fields : ( keyof InscriptionSizeAggregate ) [ ] = [
370- 'totalEnvelopeSize' , 'totalContentSize' ,
371- 'largestEnvelopeSize' , 'largestContentSize' ,
372- 'largestEnvelopeInscriptionId' , 'largestContentInscriptionId' ,
373- 'averageEnvelopeSize' , 'averageContentSize' ,
374- ] ;
375- return fields . map ( f => ( {
376- col : `${ colPrefix } _${ camelToSnake ( f as string ) } ` ,
377- alias : aliasPrefix + ( f as string ) . charAt ( 0 ) . toUpperCase ( ) + ( f as string ) . slice ( 1 ) ,
378- val : s => pick ( s ) [ f ] ,
379- set : ( t , v ) => { ( pickTarget ( t ) as any ) [ f ] = v ; } ,
380- } ) ) ;
381- } ;
361+ ) : OrdpoolStatColumn [ ] =>
362+ sectionCols ( colPrefix , aliasPrefix , pick , INSCRIPTION_SIZE_FIELDS ) ;
382363
383- const truncated20 = (
364+ const truncatedMostActive = (
384365 col : string ,
385366 alias : string ,
386367 val : ( s : OrdpoolStats ) => unknown ,
387368 set : ( t : OrdpoolStats , v : any ) => void ,
388- ) : OrdpoolStatColumn => ( { col, alias, placeholder : 'LEFT(?, 20)' , val, set } ) ;
369+ ) : OrdpoolStatColumn => ( { col, alias, placeholder : TRUNCATED_PLACEHOLDER , val, set } ) ;
389370
390371export const ORDPOOL_STATS_COLUMNS : OrdpoolStatColumn [ ] = [
391- ...amountCols (
372+ ...sectionCols ( 'amounts' , 'amounts' , s => s . amounts , [
392373 'atomical' , 'atomicalMint' , 'atomicalUpdate' ,
393374 'counterparty' , 'stamp' , 'src721' , 'src101' ,
394375 'cat21' , 'cat21Mint' ,
395376 'inscription' , 'inscriptionMint' , 'inscriptionImage' , 'inscriptionText' , 'inscriptionJson' ,
396377 'rune' , 'runeEtch' , 'runeMint' , 'runeCenotaph' ,
397378 'brc20' , 'brc20Deploy' , 'brc20Mint' , 'brc20Transfer' ,
398379 'src20' , 'src20Deploy' , 'src20Mint' , 'src20Transfer' ,
399- ) ,
400- ...feeCols (
380+ ] ) ,
381+ ...sectionCols ( 'fees' , 'fees' , s => s . fees , [
401382 'runeMints' , 'nonUncommonRuneMints' , 'brc20Mints' , 'src20Mints' ,
402383 'cat21Mints' , 'atomicals' , 'inscriptionMints' ,
403384 'inscriptionImageMints' , 'inscriptionTextMints' , 'inscriptionJsonMints' ,
404- ) ,
405- ...inscriptionSizeCols ( 'inscriptions' , 'inscriptions' , s => s . inscriptions , t => t . inscriptions ) ,
406- ...inscriptionSizeCols ( 'inscriptions_image' , 'inscriptionsImage' , s => s . inscriptions . image , t => t . inscriptions . image ) ,
407- ...inscriptionSizeCols ( 'inscriptions_text' , 'inscriptionsText' , s => s . inscriptions . text , t => t . inscriptions . text ) ,
408- ...inscriptionSizeCols ( 'inscriptions_json' , 'inscriptionsJson' , s => s . inscriptions . json , t => t . inscriptions . json ) ,
409- {
410- col : 'inscriptions_brotli_count' , alias : 'inscriptionsBrotliCount' ,
411- val : s => s . inscriptions . brotliCount ,
412- set : ( t , v ) => { t . inscriptions . brotliCount = v ; } ,
413- } ,
414- {
415- col : 'inscriptions_gzip_count' , alias : 'inscriptionsGzipCount' ,
416- val : s => s . inscriptions . gzipCount ,
417- set : ( t , v ) => { t . inscriptions . gzipCount = v ; } ,
418- } ,
419- {
420- col : 'inscriptions_compressed_envelope_bytes' , alias : 'inscriptionsCompressedEnvelopeBytes' ,
421- val : s => s . inscriptions . compressedEnvelopeBytes ,
422- set : ( t , v ) => { t . inscriptions . compressedEnvelopeBytes = v ; } ,
423- } ,
424- ...cat21Cols ( 'genesisCount' , 'avgFeeRate' , 'minFeeRate' , 'maxFeeRate' ) ,
425- ...runeCols ( 'uniqueMintsCount' , 'uniqueMintsCountNonUncommon' , 'topMintCount' , 'topMintCountNonUncommon' ) ,
426- truncated20 ( 'runes_most_active_mint' , 'runesMostActiveMint' ,
385+ ] ) ,
386+ ...inscriptionSizeCols ( 'inscriptions' , 'inscriptions' , s => s . inscriptions ) ,
387+ ...inscriptionSizeCols ( 'inscriptions_image' , 'inscriptionsImage' , s => s . inscriptions . image ) ,
388+ ...inscriptionSizeCols ( 'inscriptions_text' , 'inscriptionsText' , s => s . inscriptions . text ) ,
389+ ...inscriptionSizeCols ( 'inscriptions_json' , 'inscriptionsJson' , s => s . inscriptions . json ) ,
390+ ...sectionCols ( 'inscriptions' , 'inscriptions' , s => s . inscriptions , [
391+ 'brotliCount' , 'gzipCount' , 'compressedEnvelopeBytes' ,
392+ ] ) ,
393+ ...sectionCols ( 'cat21' , 'cat21' , s => s . cat21 , [
394+ 'genesisCount' , 'avgFeeRate' , 'minFeeRate' , 'maxFeeRate' ,
395+ ] ) ,
396+ ...sectionCols ( 'runes' , 'runes' , s => s . runes , [
397+ 'uniqueMintsCount' , 'uniqueMintsCountNonUncommon' , 'topMintCount' , 'topMintCountNonUncommon' ,
398+ ] ) ,
399+ truncatedMostActive ( 'runes_most_active_mint' , 'runesMostActiveMint' ,
427400 s => s . runes . mostActiveMint , ( t , v ) => { t . runes . mostActiveMint = v ; } ) ,
428- truncated20 ( 'runes_most_active_non_uncommon_mint' , 'runesMostActiveNonUncommonMint' ,
401+ truncatedMostActive ( 'runes_most_active_non_uncommon_mint' , 'runesMostActiveNonUncommonMint' ,
429402 s => s . runes . mostActiveNonUncommonMint , ( t , v ) => { t . runes . mostActiveNonUncommonMint = v ; } ) ,
430- truncated20 ( 'brc20_most_active_mint' , 'brc20MostActiveMint' ,
403+ truncatedMostActive ( 'brc20_most_active_mint' , 'brc20MostActiveMint' ,
431404 s => s . brc20 . mostActiveMint , ( t , v ) => { t . brc20 . mostActiveMint = v ; } ) ,
432- truncated20 ( 'src20_most_active_mint' , 'src20MostActiveMint' ,
405+ truncatedMostActive ( 'src20_most_active_mint' , 'src20MostActiveMint' ,
433406 s => s . src20 . mostActiveMint , ( t , v ) => { t . src20 . mostActiveMint = v ; } ) ,
407+ // analyser_version doubles as the "is this row populated?" sentinel —
408+ // formatDbBlockIntoOrdpoolStats returns undefined when it's 0.
434409 {
435410 col : 'analyser_version' , alias : 'analyserVersion' ,
436411 val : s => s . version ,
437412 set : ( t , v ) => { t . version = v ; } ,
438413 } ,
439414] ;
440415
416+ // Static parts of the INSERT — column list, placeholder list, and the SQL
417+ // string itself. Computed once at module load. Per-call work in
418+ // saveBlockOrdpoolStatsInDatabase is reduced to one .map() over the spec
419+ // to materialise the param values.
420+ const ORDPOOL_STATS_INSERT_SQL = ( ( ) => {
421+ const cols = [ 'hash' , 'height' , ...ORDPOOL_STATS_COLUMNS . map ( c => c . col ) ] ;
422+ const phs = [ '?' , '?' , ...ORDPOOL_STATS_COLUMNS . map ( c => c . placeholder ?? '?' ) ] ;
423+ return `INSERT INTO ordpool_stats(${ cols . join ( ', ' ) } ) VALUES (${ phs . join ( ', ' ) } )` ;
424+ } ) ( ) ;
425+
441426
442427class OrdpoolBlocksRepository {
443428 /**
@@ -461,16 +446,11 @@ class OrdpoolBlocksRepository {
461446
462447 const stats = block . extras . ordpoolStats ;
463448
464- // Single source of truth: column list, placeholder list, and params
465- // array all derive from ORDPOOL_STATS_COLUMNS in the same .map() pass,
466- // so positional drift between them is impossible.
467- const cols = [ 'hash' , 'height' , ...ORDPOOL_STATS_COLUMNS . map ( c => c . col ) ] ;
468- const placeholders = [ '?' , '?' , ...ORDPOOL_STATS_COLUMNS . map ( c => c . placeholder ?? '?' ) ] ;
469- const params = [ block . id , block . height , ...ORDPOOL_STATS_COLUMNS . map ( c => c . val ( stats ) ) ] ;
470-
471- const query = `INSERT INTO ordpool_stats(${ cols . join ( ', ' ) } ) VALUES (${ placeholders . join ( ', ' ) } )` ;
472-
473- await DB . query ( query , params , 'silent' ) ;
449+ // SQL is precomputed at module load; per-call work is just materialising
450+ // the param values. Positional alignment is preserved because both
451+ // the SQL and the params iterate ORDPOOL_STATS_COLUMNS in the same order.
452+ const params = [ block . id , block . height , ...ORDPOOL_STATS_COLUMNS . map ( c => c . val ( stats ) ) ] ;
453+ await DB . query ( ORDPOOL_STATS_INSERT_SQL , params , 'silent' ) ;
474454
475455 logger . debug ( `$saveBlockOrdpoolStatsInDatabase() - Block ${ block . height } successfully stored!` , 'Ordpool' ) ;
476456
@@ -508,9 +488,6 @@ class OrdpoolBlocksRepository {
508488 result . src20 . src20MintActivity = compactToMintActivity ( dbBlk . src20MintActivity ) ;
509489 result . src20 . src20DeployAttempts = compactToSrc20DeployAttempts ( dbBlk . src20DeployAttempts ) ;
510490 result . cat21 . minimalCat21MintActivity = compactToMinimalCat21Mints ( dbBlk . cat21MintActivity ) ;
511- // Block-detail responses don't carry the atomical_op /
512- // counterparty per-row satellite arrays — chart endpoints
513- // query those tables directly via GROUP BY.
514491
515492 return result ;
516493 }
0 commit comments