@@ -24,8 +24,10 @@ import (
2424 "io"
2525 "io/fs"
2626 "mime"
27+ "net"
2728 "net/http"
2829 "runtime/debug"
30+ "strings"
2931 "time"
3032
3133 ouroboros "github.com/blinklabs-io/gouroboros"
@@ -34,8 +36,8 @@ import (
3436 _ "github.com/blinklabs-io/tx-submit-api/docs" // docs is generated by Swag CLI
3537 "github.com/blinklabs-io/tx-submit-api/internal/config"
3638 "github.com/blinklabs-io/tx-submit-api/internal/logging"
39+ "github.com/blinklabs-io/tx-submit-api/internal/metrics"
3740 "github.com/blinklabs-io/tx-submit-api/submit"
38- "github.com/prometheus/client_golang/prometheus"
3941 "github.com/prometheus/client_golang/prometheus/promhttp"
4042 httpSwagger "github.com/swaggo/http-swagger"
4143)
@@ -47,23 +49,6 @@ var staticFS embed.FS
4749// submission. Cardano's protocol limit is ~16KB; 64KB gives ample overhead.
4850const maxTxBodyBytes = 64 * 1024
4951
50- var (
51- // Gauge to match input-output-hk's metric type
52- txSubmitFailCount = prometheus .NewGauge (prometheus.GaugeOpts {
53- Name : "tx_submit_fail_count" ,
54- Help : "transactions failed" ,
55- })
56- // Gauge to match input-output-hk's metric type
57- txSubmitCount = prometheus .NewGauge (prometheus.GaugeOpts {
58- Name : "tx_submit_count" ,
59- Help : "transactions submitted" ,
60- })
61- )
62-
63- func init () {
64- prometheus .MustRegister (txSubmitFailCount , txSubmitCount )
65- }
66-
6752// corsMiddleware adds CORS headers allowing all origins.
6853func corsMiddleware (next http.Handler ) http.Handler {
6954 return http .HandlerFunc (func (w http.ResponseWriter , r * http.Request ) {
@@ -228,6 +213,8 @@ func Start(cfg *config.Config) error {
228213 handler = recoveryMiddleware (handler )
229214 handler = corsMiddleware (handler )
230215
216+ metrics .Register ()
217+
231218 // Start metrics listener
232219 go func () {
233220 logger .Info ("starting metrics listener" ,
@@ -352,16 +339,17 @@ func handleHasTx(w http.ResponseWriter, r *http.Request) {
352339// @Failure 500 {object} string "Server Error"
353340// @Router /api/submit/tx [post]
354341func handleSubmitTx (w http.ResponseWriter , r * http.Request ) {
355- // First, initialize our configuration and loggers
356342 cfg := config .GetConfig ()
357343 logger := logging .GetLogger ()
344+ clientIP := realClientIP (r )
358345
359- // Check our headers for content-type
346+ // Check our headers for content-type. Wrong content-type is rejected before
347+ // reading the body, so no IP metric is recorded here.
360348 mediaType , _ , _ := mime .ParseMediaType (r .Header .Get ("Content-Type" ))
361349 if mediaType != "application/cbor" {
362350 logger .Error ("invalid request body, should be application/cbor" )
363351 writeJSON (w , http .StatusUnsupportedMediaType , "invalid request body, should be application/cbor" )
364- txSubmitFailCount . Inc ()
352+ metrics . IncTxSubmitFailCount ()
365353 return
366354 }
367355
@@ -376,9 +364,21 @@ func handleSubmitTx(w http.ResponseWriter, r *http.Request) {
376364 logger .Error ("failed to read request body" , "err" , err )
377365 writeJSON (w , http .StatusInternalServerError , "failed to read request body" )
378366 }
379- txSubmitFailCount .Inc ()
367+ metrics .IncTxSubmitFailCount ()
368+ metrics .RecordTxRequest (clientIP , "error" )
369+ return
370+ }
371+
372+ // Parse tx content signals before submitting so we can record them regardless
373+ // of whether the node accepts or rejects the transaction.
374+ txInfo , err := submit .ParseTxInfo (txRawBytes )
375+ if err != nil {
376+ writeJSON (w , http .StatusBadRequest , err .Error ())
377+ metrics .IncTxSubmitFailCount ()
378+ metrics .RecordTxRequest (clientIP , "error" )
380379 return
381380 }
381+
382382 // Send TX
383383 errorChan := make (chan error , 1 )
384384 submitConfig := & submit.Config {
@@ -391,9 +391,10 @@ func handleSubmitTx(w http.ResponseWriter, r *http.Request) {
391391 }
392392 txHash , err := submit .SubmitTx (submitConfig , txRawBytes )
393393 if err != nil {
394+ var txRejectErr * localtxsubmission.TransactionRejectedError
395+ isRejected := errors .As (err , & txRejectErr )
394396 if r .Header .Get ("Accept" ) == "application/cbor" {
395- var txRejectErr * localtxsubmission.TransactionRejectedError
396- if errors .As (err , & txRejectErr ) {
397+ if isRejected && txRejectErr != nil {
397398 w .Header ().Set ("Content-Type" , "application/cbor" )
398399 w .WriteHeader (http .StatusBadRequest )
399400 _ , _ = w .Write (txRejectErr .ReasonCbor )
@@ -405,13 +406,21 @@ func handleSubmitTx(w http.ResponseWriter, r *http.Request) {
405406 } else {
406407 writeJSON (w , http .StatusBadRequest , err .Error ())
407408 }
408- txSubmitFailCount .Inc ()
409+ result := "error"
410+ if isRejected {
411+ result = "rejected"
412+ }
413+ metrics .IncTxSubmitFailCount ()
414+ metrics .RecordTxRequest (clientIP , result )
415+ metrics .RecordTxContent (txInfo .ScriptType , txInfo .HasMinting , txInfo .HasReferenceInputs )
409416 return
410417 }
411418
412419 // Node confirmed the tx is in its mempool (AcceptTx received synchronously).
413420 // Record success before responding so metrics always agree with the HTTP status.
414- txSubmitCount .Inc ()
421+ metrics .IncTxSubmitCount ()
422+ metrics .RecordTxRequest (clientIP , "accepted" )
423+ metrics .RecordTxContent (txInfo .ScriptType , txInfo .HasMinting , txInfo .HasReferenceInputs )
415424 writeJSON (w , http .StatusAccepted , txHash )
416425
417426 // Drain errorChan in the background. Post-submission connection errors do not
@@ -427,3 +436,20 @@ func handleSubmitTx(w http.ResponseWriter, r *http.Request) {
427436 }
428437 }()
429438}
439+
440+ // realClientIP extracts the real client IP from the request, accounting for
441+ // reverse proxies. Priority: X-Real-IP > first entry of X-Forwarded-For > RemoteAddr.
442+ func realClientIP (r * http.Request ) string {
443+ if ip := strings .TrimSpace (r .Header .Get ("X-Real-IP" )); ip != "" {
444+ return ip
445+ }
446+ if forwarded := r .Header .Get ("X-Forwarded-For" ); forwarded != "" {
447+ first , _ , _ := strings .Cut (forwarded , "," )
448+ return strings .TrimSpace (first )
449+ }
450+ host , _ , err := net .SplitHostPort (r .RemoteAddr )
451+ if err != nil {
452+ return r .RemoteAddr
453+ }
454+ return host
455+ }
0 commit comments