diff --git a/internal/api/api.go b/internal/api/api.go index 78d76c7..0f6345a 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -20,11 +20,7 @@ import ( "time" ouroboros "github.com/blinklabs-io/gouroboros" - "github.com/blinklabs-io/gouroboros/ledger" "github.com/blinklabs-io/gouroboros/protocol/localtxsubmission" - "github.com/blinklabs-io/tx-submit-api/internal/config" - "github.com/blinklabs-io/tx-submit-api/internal/logging" - "github.com/fxamacker/cbor/v2" ginzap "github.com/gin-contrib/zap" "github.com/gin-gonic/gin" @@ -33,6 +29,10 @@ import ( ginSwagger "github.com/swaggo/gin-swagger" // gin-swagger middleware _ "github.com/blinklabs-io/tx-submit-api/docs" // docs is generated by Swag CLI + "github.com/blinklabs-io/tx-submit-api/internal/config" + "github.com/blinklabs-io/tx-submit-api/internal/logging" + "github.com/blinklabs-io/tx-submit-api/submit" + ) // @title tx-submit-api @@ -45,8 +45,8 @@ import ( // @contact.url https://blinklabs.io // @contact.email support@blinklabs.io -// @license.name Apache 2.0 -// @license.url http://www.apache.org/licenses/LICENSE-2.0.html +// @license.name Apache 2.0 +// @license.url http://www.apache.org/licenses/LICENSE-2.0.html func Start(cfg *config.Config) error { // Disable gin debug and color output gin.SetMode(gin.ReleaseMode) @@ -259,58 +259,31 @@ func handleSubmitTx(c *gin.Context) { if err := c.Request.Body.Close(); err != nil { logger.Errorf("failed to close request body: %s", err) } - // Determine transaction type (era) - txType, err := ledger.DetermineTransactionType(txRawBytes) - if err != nil { - logger.Errorf("could not parse transaction to determine type: %s", err) - c.JSON(400, "could not parse transaction to determine type") - _ = ginmetrics.GetMonitor().GetMetric("tx_submit_fail_count").Inc(nil) - return - } - tx, err := ledger.NewTransactionFromCbor(txType, txRawBytes) - if err != nil { - logger.Errorf("failed to parse transaction CBOR: %s", err) - c.JSON(400, fmt.Sprintf("failed to parse transaction CBOR: %s", err)) - _ = ginmetrics.GetMonitor().GetMetric("tx_submit_fail_count").Inc(nil) - return - } - // Connect to cardano-node and submit TX + // Send TX errorChan := make(chan error) - oConn, err := ouroboros.NewConnection( - ouroboros.WithNetworkMagic(uint32(cfg.Node.NetworkMagic)), - ouroboros.WithErrorChan(errorChan), - ouroboros.WithNodeToNode(false), - ouroboros.WithLocalTxSubmissionConfig( - localtxsubmission.NewConfig( - localtxsubmission.WithTimeout( - time.Duration(cfg.Node.Timeout)*time.Second, - ), - ), - ), - ) + submitConfig := &submit.Config{ + ErrorChan: errorChan, + NetworkMagic: cfg.Node.NetworkMagic, + NodeAddress: cfg.Node.Address, + NodePort: cfg.Node.Port, + SocketPath: cfg.Node.SocketPath, + Timeout: cfg.Node.Timeout, + } + txHash, err := submit.SubmitTx(submitConfig, txRawBytes) if err != nil { - logger.Errorf("failure creating Ouroboros connection: %s", err) - c.JSON(500, "failure communicating with node") + if c.GetHeader("Accept") == "application/cbor" { + txRejectErr := err.(localtxsubmission.TransactionRejectedError) + c.Data(400, "application/cbor", txRejectErr.ReasonCbor) + } else { + if err.Error() != "" { + c.JSON(400, err.Error()) + } else { + c.JSON(400, fmt.Sprintf("%s", err)) + } + } _ = ginmetrics.GetMonitor().GetMetric("tx_submit_fail_count").Inc(nil) return } - if cfg.Node.Address != "" && cfg.Node.Port > 0 { - if err := oConn.Dial("tcp", fmt.Sprintf("%s:%d", cfg.Node.Address, cfg.Node.Port)); err != nil { - logger.Errorf("failure connecting to node via TCP: %s", err) - c.JSON(500, "failure communicating with node") - _ = ginmetrics.GetMonitor(). - GetMetric("tx_submit_fail_count"). - Inc(nil) - return - } - } else { - if err := oConn.Dial("unix", cfg.Node.SocketPath); err != nil { - logger.Errorf("failure connecting to node via UNIX socket: %s", err) - c.JSON(500, "failure communicating with node") - _ = ginmetrics.GetMonitor().GetMetric("tx_submit_fail_count").Inc(nil) - return - } - } // Start async error handler go func() { err, ok := <-errorChan @@ -322,24 +295,8 @@ func handleSubmitTx(c *gin.Context) { Inc(nil) } }() - defer func() { - // Close Ouroboros connection - oConn.Close() - }() - // Submit the transaction - if err := oConn.LocalTxSubmission().Client.SubmitTx(uint16(txType), txRawBytes); err != nil { - if c.GetHeader("Accept") == "application/cbor" { - txRejectErr := err.(localtxsubmission.TransactionRejectedError) - c.Data(400, "application/cbor", txRejectErr.ReasonCbor) - } else { - c.JSON(400, err.Error()) - } - // Increment custom metric - _ = ginmetrics.GetMonitor().GetMetric("tx_submit_fail_count").Inc(nil) - return - } // Return transaction ID - c.JSON(202, tx.Hash()) + c.JSON(202, txHash) // Increment custom metric _ = ginmetrics.GetMonitor().GetMetric("tx_submit_count").Inc(nil) } diff --git a/submit/tx.go b/submit/tx.go new file mode 100644 index 0000000..4f6c2de --- /dev/null +++ b/submit/tx.go @@ -0,0 +1,100 @@ +// Copyright 2023 Blink Labs Software +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package submit + +import ( + "fmt" + "time" + + ouroboros "github.com/blinklabs-io/gouroboros" + "github.com/blinklabs-io/gouroboros/ledger" + "github.com/blinklabs-io/gouroboros/protocol/localtxsubmission" +) + +type Config struct { + ErrorChan chan error + Network string + NetworkMagic uint32 + NodeAddress string + NodePort uint + SocketPath string + Timeout uint +} + +func SubmitTx(cfg *Config, txRawBytes []byte) (string, error) { + // Determine transaction type (era) + txType, err := ledger.DetermineTransactionType(txRawBytes) + if err != nil { + return "", fmt.Errorf("could not parse transaction to determine type: %s", err) + } + tx, err := ledger.NewTransactionFromCbor(txType, txRawBytes) + if err != nil { + return "", fmt.Errorf("failed to parse transaction CBOR: %s", err) + } + + err = cfg.populateNetworkMagic() + if err != nil { + return "", fmt.Errorf("failed to populate networkMagic: %s", err) + } + + // Connect to cardano-node and submit TX using Ouroboros LocalTxSubmission + oConn, err := ouroboros.NewConnection( + ouroboros.WithNetworkMagic(uint32(cfg.NetworkMagic)), + ouroboros.WithErrorChan(cfg.ErrorChan), + ouroboros.WithNodeToNode(false), + ouroboros.WithLocalTxSubmissionConfig( + localtxsubmission.NewConfig( + localtxsubmission.WithTimeout( + time.Duration(cfg.Timeout)*time.Second, + ), + ), + ), + ) + if err != nil { + return "", fmt.Errorf("failure creating Ouroboros connection: %s", err) + } + if cfg.NodeAddress != "" && cfg.NodePort > 0 { + if err := oConn.Dial("tcp", fmt.Sprintf("%s:%d", cfg.NodeAddress, cfg.NodePort)); err != nil { + return "", fmt.Errorf("failure connecting to node via TCP: %s", err) + } + } else { + if err := oConn.Dial("unix", cfg.SocketPath); err != nil { + return "", fmt.Errorf("failure connecting to node via UNIX socket: %s", err) + } + } + defer func() { + // Close Ouroboros connection + oConn.Close() + }() + // Submit the transaction + if err := oConn.LocalTxSubmission().Client.SubmitTx(uint16(txType), txRawBytes); err != nil { + return "", fmt.Errorf("%s", err.Error()) + } + return tx.Hash(), nil +} + +func (c *Config) populateNetworkMagic() error { + if c.NetworkMagic == 0 { + if c.Network != "" { + network := ouroboros.NetworkByName(c.Network) + if network == ouroboros.NetworkInvalid { + return fmt.Errorf("unknown network: %s", c.Network) + } + c.NetworkMagic = uint32(network.NetworkMagic) + return nil + } + } + return nil +}