Repository: bitcoin-sv/arc
Pull request #850: feat(BPAAS-2067): combine endpoint processing
Author: login: shotasilagadzetaal
Created: 2025-04-18T14:52:51Z | Merged: 2025-04-28T09:06:40Z

## Description of Changes

Provide a brief description of the changes you've made.

## Testing Procedure

- [ ] I have added new unit tests
- [ ] All tests pass locally
- [ ] I have tested manually in my local environment

## Checklist:

- [ ] I have performed a self-review of my own code
- [ ] I have made corresponding changes to the documentation
- [ ] I have updated `CHANGELOG.md` with my changes



Diff:
diff --git a/internal/api/handler/default.go b/internal/api/handler/default.go
index 5b984e63f..c32d6bfc6 100644
--- a/internal/api/handler/default.go
+++ b/internal/api/handler/default.go
@@ -20,7 +20,6 @@ import (
 	"github.com/bitcoin-sv/arc/internal/api/handler/internal/merkle_verifier"
 	"github.com/bitcoin-sv/arc/internal/beef"
 	"github.com/bitcoin-sv/arc/internal/blocktx"
-	"github.com/bitcoin-sv/arc/internal/logger"
 	"github.com/bitcoin-sv/arc/internal/metamorph"
 	"github.com/bitcoin-sv/arc/internal/metamorph/metamorph_api"
 	"github.com/bitcoin-sv/arc/internal/validator"
@@ -216,117 +215,6 @@ func calcFeesFromBSVPerKB(feePerKB float64) (uint64, uint64) {
 	return satoshis, bytes
 }
 
-func (m ArcDefaultHandler) postTransaction(ctx echo.Context, params api.POSTTransactionParams) PostResponse {
-	var err error
-	attributes := m.tracingAttributes
-	if eventID, ok := ctx.Request().Context().Value(logger.EventIDField).(string); ok {
-		attributes = append(m.tracingAttributes, attribute.String(logger.EventID, eventID))
-	}
-	reqCtx, span := tracing.StartTracing(ctx.Request().Context(), "POSTTransaction", m.tracingEnabled, attributes...)
-	defer func() {
-		tracing.EndTracing(span, err)
-	}()
-
-	transactionOptions, err := getTransactionOptions(params, m.rejectedCallbackURLSubstrings)
-	if err != nil {
-		e := api.NewErrorFields(api.ErrStatusBadRequest, err.Error())
-		return PostResponse{e.Status, e}
-	}
-
-	txHex, err := parseTransactionFromRequest(ctx.Request())
-	if err != nil {
-		e := api.NewErrorFields(api.ErrStatusBadRequest, fmt.Sprintf("error parsing transaction from request: %s", err.Error()))
-		if span != nil {
-			attr := e.GetSpanAttributes()
-			span.SetAttributes(attr...)
-		}
-		return PostResponse{e.Status, e}
-	}
-
-	// Now we check if we have the transaction present in db, if so we skip validation (as we must have already validated it)
-	// if LastSubmitted is not too old and callbacks are the same then we just stop processing transaction as there is nothing new
-	txIDs, e := m.getTxIDs(txHex)
-	if e != nil {
-		if span != nil {
-			attr := e.GetSpanAttributes()
-			span.SetAttributes(attr...)
-		}
-		// if an error is returned, the processing failed
-		return PostResponse{e.Status, e}
-	}
-
-	if !transactionOptions.ForceValidation {
-		// check if we already have the transaction in db (so no need to validate)
-		tx, err := m.getTransactionStatus(reqCtx, txIDs[0])
-		if err != nil {
-			// if we have error which is NOT ErrTransactionNotFound, return err
-			if !errors.Is(err, metamorph.ErrTransactionNotFound) {
-				m.logger.Error("Failed to get transaction status", slog.String("hash", txIDs[0]), slog.String("err", err.Error()))
-			}
-		} else {
-			// if transaction was found skip the validation
-			transactionOptions.SkipTxValidation = true
-
-			// check if all callbacks already exist
-			callbackAlreadyExists := true
-
-			if transactionOptions.CallbackURL != "" || transactionOptions.CallbackToken != "" {
-				for _, cb := range tx.Callbacks {
-					callbackAlreadyExists = false
-					if cb.CallbackUrl == transactionOptions.CallbackURL && cb.CallbackToken == transactionOptions.CallbackToken {
-						callbackAlreadyExists = true
-						break
-					}
-				}
-			}
-
-			// if tx has been last submitted less than expiry time ago and callbacks already exist, return current status
-			if time.Since(tx.LastSubmitted.AsTime()) < m.mapExpiryTime && callbackAlreadyExists {
-				return PostResponse{int(api.StatusOK), &api.TransactionResponse{
-					Status:       int(api.StatusOK),
-					Title:        "OK",
-					BlockHash:    &tx.BlockHash,
-					BlockHeight:  &tx.BlockHeight,
-					TxStatus:     (api.TransactionResponseTxStatus)(tx.Status),
-					ExtraInfo:    &tx.ExtraInfo,
-					CompetingTxs: &tx.CompetingTxs,
-					Timestamp:    m.now(),
-					Txid:         txIDs[0],
-					MerklePath:   &tx.MerklePath,
-				}}
-			}
-		}
-	}
-
-	successes, fails, e := m.processTransactions(reqCtx, txHex, transactionOptions)
-	if e != nil {
-		if span != nil {
-			attr := e.GetSpanAttributes()
-			span.SetAttributes(attr...)
-		}
-		// if an error is returned, the processing failed
-		return PostResponse{e.Status, e}
-	}
-
-	if len(fails) > 0 {
-		// if a fail result is returned, the processing/validation failed
-		e = fails[0]
-		if span != nil {
-			attr := e.GetSpanAttributes()
-			span.SetAttributes(attr...)
-		}
-		return PostResponse{e.Status, e}
-	}
-
-	res := successes[0]
-
-	if span != nil {
-		span.SetAttributes(attribute.String("status", string(res.TxStatus)))
-	}
-
-	return PostResponse{res.Status, res}
-}
-
 // POSTTransaction ...
 func (m ArcDefaultHandler) POSTTransaction(ctx echo.Context, params api.POSTTransactionParams) (err error) {
 	timeout := m.defaultTimeout
@@ -342,7 +230,36 @@ func (m ArcDefaultHandler) POSTTransaction(ctx echo.Context, params api.POSTTran
 	ctx.SetRequest(ctx.Request().WithContext(timeoutCtx))
 	defer cancel()
 
-	postResponse := m.postTransaction(ctx, params)
+	txsHex, err := parseTransactionFromRequest(ctx.Request())
+	if err != nil {
+		e := api.NewErrorFields(api.ErrStatusBadRequest, fmt.Sprintf("error parsing transactions from request: %s", err.Error()))
+		res := PostResponse{e.Status, e}
+		return ctx.JSON(res.StatusCode, res.response)
+	}
+	txsParams := api.POSTTransactionsParams(params)
+	postResponse := m.postTransactions(ctx, txsHex, txsParams)
+
+	switch postResponse.response.(type) {
+	case []interface{}:
+		switch postResponse.response.([]interface{})[0].(type) {
+		case *api.TransactionResponse:
+			res, ok := postResponse.response.([]interface{})[0].(*api.TransactionResponse)
+			if ok {
+				return ctx.JSON(res.Status, res)
+			}
+		case *api.ErrorFields:
+			res, ok := postResponse.response.([]interface{})[0].(*api.ErrorFields)
+			if ok {
+				return ctx.JSON(res.Status, res)
+			}
+		}
+
+	case *api.ErrorFields:
+		res, ok := postResponse.response.(*api.ErrorFields)
+		if ok {
+			return ctx.JSON(res.Status, res)
+		}
+	}
 	return ctx.JSON(postResponse.StatusCode, postResponse.response)
 }
 
@@ -391,7 +308,7 @@ func (m ArcDefaultHandler) GETTransactionStatus(ctx echo.Context, id string) (er
 	})
 }
 
-func (m ArcDefaultHandler) postTransactions(ctx echo.Context, params api.POSTTransactionsParams) PostResponse {
+func (m ArcDefaultHandler) postTransactions(ctx echo.Context, txsHex []byte, params api.POSTTransactionsParams) PostResponse {
 	var err error
 	reqCtx, span := tracing.StartTracing(ctx.Request().Context(), "POSTTransactions", m.tracingEnabled, m.tracingAttributes...)
 	defer func() {
@@ -409,16 +326,6 @@ func (m ArcDefaultHandler) postTransactions(ctx echo.Context, params api.POSTTra
 		return PostResponse{e.Status, e}
 	}
 
-	txsHex, err := parseTransactionsFromRequest(ctx.Request())
-	if err != nil {
-		e := api.NewErrorFields(api.ErrStatusBadRequest, fmt.Sprintf("error parsing transaction from request: %s", err.Error()))
-		if span != nil {
-			attr := e.GetSpanAttributes()
-			span.SetAttributes(attr...)
-		}
-		return PostResponse{e.Status, e}
-	}
-
 	// Now we check if we have the transactions present in db, if so we skip validation (as we must have already validated them)
 	// if LastSubmitted is not too old and callbacks are the same then we just stop processing transactions as there is nothing new
 	txIDs, e := m.getTxIDs(txsHex)
@@ -529,12 +436,15 @@ func (m ArcDefaultHandler) POSTTransactions(ctx echo.Context, params api.POSTTra
 	ctx.SetRequest(ctx.Request().WithContext(timeoutCtx))
 	defer cancel()
 
-	postResponse := m.postTransactions(ctx, params)
-	return ctx.JSON(postResponse.StatusCode, postResponse.response)
-}
+	txsHex, err := parseTransactionsFromRequest(ctx.Request())
+	if err != nil {
+		e := api.NewErrorFields(api.ErrStatusBadRequest, fmt.Sprintf("error parsing transactions from request: %s", err.Error()))
+		res := PostResponse{e.Status, e}
+		return ctx.JSON(res.StatusCode, res.response)
+	}
 
-func getTransactionOptions(params api.POSTTransactionParams, rejectedCallbackURLSubstrings []string) (*metamorph.TransactionOptions, error) {
-	return getTransactionsOptions(api.POSTTransactionsParams(params), rejectedCallbackURLSubstrings)
+	postResponse := m.postTransactions(ctx, txsHex, params)
+	return ctx.JSON(postResponse.StatusCode, postResponse.response)
 }
 
 func ValidateCallbackURL(callbackURL string, rejectedCallbackURLSubstrings []string) error {
diff --git a/internal/api/handler/default_test.go b/internal/api/handler/default_test.go
index a1f6e452f..6fa98eded 100644
--- a/internal/api/handler/default_test.go
+++ b/internal/api/handler/default_test.go
@@ -381,7 +381,7 @@ func TestPOSTTransaction(t *testing.T) { //nolint:funlen
 			contentType: contentTypes[0],
 
 			expectedStatus:   400,
-			expectedResponse: *api.NewErrorFields(api.ErrStatusBadRequest, "error parsing transaction from request: no transaction found - empty request body"),
+			expectedResponse: *api.NewErrorFields(api.ErrStatusBadRequest, "error parsing transactions from request: no transaction found - empty request body"),
 			expectedError:    ErrEmptyBody,
 		},
 		{
@@ -389,7 +389,7 @@ func TestPOSTTransaction(t *testing.T) { //nolint:funlen
 			contentType: contentTypes[1],
 
 			expectedStatus:   400,
-			expectedResponse: *api.NewErrorFields(api.ErrStatusBadRequest, "error parsing transaction from request: no transaction found - empty request body"),
+			expectedResponse: *api.NewErrorFields(api.ErrStatusBadRequest, "error parsing transactions from request: no transaction found - empty request body"),
 			expectedError:    ErrEmptyBody,
 		},
 		{
@@ -397,7 +397,7 @@ func TestPOSTTransaction(t *testing.T) { //nolint:funlen
 			contentType: contentTypes[2],
 
 			expectedStatus:   400,
-			expectedResponse: *api.NewErrorFields(api.ErrStatusBadRequest, "error parsing transaction from request: no transaction found - empty request body"),
+			expectedResponse: *api.NewErrorFields(api.ErrStatusBadRequest, "error parsing transactions from request: no transaction found - empty request body"),
 			expectedError:    ErrEmptyBody,
 		},
 		{
@@ -406,7 +406,7 @@ func TestPOSTTransaction(t *testing.T) { //nolint:funlen
 			txHexString: validTx,
 
 			expectedStatus:   400,
-			expectedResponse: *api.NewErrorFields(api.ErrStatusBadRequest, "error parsing transaction from request: given content-type application/xml does not match any of the allowed content-types"),
+			expectedResponse: *api.NewErrorFields(api.ErrStatusBadRequest, "error parsing transactions from request: given content-type application/xml does not match any of the allowed content-types"),
 		},
 		{
 			name:        "invalid tx - text/plain",
@@ -414,7 +414,7 @@ func TestPOSTTransaction(t *testing.T) { //nolint:funlen
 			txHexString: "test",
 
 			expectedStatus:   400,
-			expectedResponse: *api.NewErrorFields(api.ErrStatusBadRequest, "error parsing transaction from request: encoding/hex: invalid byte: U+0074 't'"),
+			expectedResponse: *api.NewErrorFields(api.ErrStatusBadRequest, "error parsing transactions from request: encoding/hex: invalid byte: U+0074 't'"),
 		},
 		{
 			name:        "invalid json - application/json",
@@ -422,7 +422,7 @@ func TestPOSTTransaction(t *testing.T) { //nolint:funlen
 			txHexString: "test",
 
 			expectedStatus:   400,
-			expectedResponse: *api.NewErrorFields(api.ErrStatusBadRequest, "error parsing transaction from request: invalid character 'e' in literal true (expecting 'r')"),
+			expectedResponse: *api.NewErrorFields(api.ErrStatusBadRequest, "error parsing transactions from request: invalid character 'e' in literal true (expecting 'r')"),
 		},
 		{
 			name:        "invalid tx - incorrect json field, application/json",
@@ -430,7 +430,7 @@ func TestPOSTTransaction(t *testing.T) { //nolint:funlen
 			txHexString: fmt.Sprintf("{\"txHex\": \"%s\"}", validTx),
 
 			expectedStatus:   400,
-			expectedResponse: *api.NewErrorFields(api.ErrStatusBadRequest, "error parsing transaction from request: no transaction found - empty request body"),
+			expectedResponse: *api.NewErrorFields(api.ErrStatusBadRequest, "error parsing transactions from request: no transaction found - empty request body"),
 			expectedError:    ErrEmptyBody,
 		},
 		{
@@ -652,7 +652,7 @@ func TestPOSTTransaction(t *testing.T) { //nolint:funlen
 					return nil, nil
 				},
 
-				GetTransactionStatusFunc: func(_ context.Context, _ string) (*metamorph.TransactionStatus, error) {
+				GetTransactionStatusesFunc: func(_ context.Context, _ []string) ([]*metamorph.TransactionStatus, error) {
 					return nil, metamorph.ErrTransactionNotFound
 				},
 
@@ -822,8 +822,8 @@ func TestPOSTTransactions(t *testing.T) { //nolint:funlen
 		require.NoError(t, err)
 
 		expectedErrors := map[string]string{
-			echo.MIMETextPlain:       "error parsing transaction from request: encoding/hex: invalid byte: U+0074 't'",
-			echo.MIMEApplicationJSON: "error parsing transaction from request: invalid character 'e' in literal true (expecting 'r')",
+			echo.MIMETextPlain:       "error parsing transactions from request: encoding/hex: invalid byte: U+0074 't'",
+			echo.MIMEApplicationJSON: "error parsing transactions from request: invalid character 'e' in literal true (expecting 'r')",
 			echo.MIMEOctetStream:     "",
 		}
 
@@ -1278,26 +1278,26 @@ func TestCalcFeesFromBSVPerKB(t *testing.T) {
 func TestGetTransactionOptions(t *testing.T) {
 	tt := []struct {
 		name   string
-		params api.POSTTransactionParams
+		params api.POSTTransactionsParams
 
 		expectedError   error
 		expectedOptions *metamorph.TransactionOptions
 	}{
 		{
 			name:   "no options",
-			params: api.POSTTransactionParams{},
+			params: api.POSTTransactionsParams{},
 
 			expectedOptions: &metamorph.TransactionOptions{},
 		},
 		{
 			name:   "max timeout",
-			params: api.POSTTransactionParams{},
+			params: api.POSTTransactionsParams{},
 
 			expectedOptions: &metamorph.TransactionOptions{},
 		},
 		{
 			name: "valid callback url",
-			params: api.POSTTransactionParams{
+			params: api.POSTTransactionsParams{
 				XCallbackUrl:   PtrTo("http://api.callme.com"),
 				XCallbackToken: PtrTo("1234"),
 			},
@@ -1309,7 +1309,7 @@ func TestGetTransactionOptions(t *testing.T) {
 		},
 		{
 			name: "invalid callback url",
-			params: api.POSTTransactionParams{
+			params: api.POSTTransactionsParams{
 				XCallbackUrl: PtrTo("api.callme.com"),
 			},
 
@@ -1317,7 +1317,7 @@ func TestGetTransactionOptions(t *testing.T) {
 		},
 		{
 			name: "wait for - QUEUED",
-			params: api.POSTTransactionParams{
+			params: api.POSTTransactionsParams{
 				XWaitFor: PtrTo("QUEUED"),
 			},
 
@@ -1327,7 +1327,7 @@ func TestGetTransactionOptions(t *testing.T) {
 		},
 		{
 			name: "wait for - RECEIVED",
-			params: api.POSTTransactionParams{
+			params: api.POSTTransactionsParams{
 				XWaitFor: PtrTo("RECEIVED"),
 			},
 
@@ -1337,7 +1337,7 @@ func TestGetTransactionOptions(t *testing.T) {
 		},
 		{
 			name: "wait for - SENT_TO_NETWORK",
-			params: api.POSTTransactionParams{
+			params: api.POSTTransactionsParams{
 				XWaitFor: PtrTo("SENT_TO_NETWORK"),
 			},
 
@@ -1347,7 +1347,7 @@ func TestGetTransactionOptions(t *testing.T) {
 		},
 		{
 			name: "wait for - ACCEPTED_BY_NETWORK",
-			params: api.POSTTransactionParams{
+			params: api.POSTTransactionsParams{
 				XWaitFor: PtrTo("ACCEPTED_BY_NETWORK"),
 			},
 
@@ -1357,7 +1357,7 @@ func TestGetTransactionOptions(t *testing.T) {
 		},
 		{
 			name: "wait for - SEEN_ON_NETWORK",
-			params: api.POSTTransactionParams{
+			params: api.POSTTransactionsParams{
 				XWaitFor: PtrTo("SEEN_ON_NETWORK"),
 			},
 
@@ -1369,7 +1369,7 @@ func TestGetTransactionOptions(t *testing.T) {
 
 	for _, tc := range tt {
 		t.Run(tc.name, func(t *testing.T) {
-			options, actualErr := getTransactionOptions(tc.params, make([]string, 0))
+			options, actualErr := getTransactionsOptions(tc.params, make([]string, 0))
 
 			if tc.expectedError != nil {
 				require.ErrorIs(t, actualErr, tc.expectedError)


Reviews:
user: login: shotasilagadzetaal
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-19T15:05:49Z
user: login: boecklim
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-22T12:28:36Z
user: login: boecklim
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-04-24T13:49:40Z

Review comments:
diff_hunk: @@ -406,16 +436,6 @@ func (m ArcDefaultHandler) postTransactions(ctx echo.Context, params api.POSTTra
 		return PostResponse{e.Status, e}
 	}
 
-	txsHex, err := parseTransactionsFromRequest(ctx.Request())
path: internal/api/handler/default.go
user: login: shotasilagadzetaal
body: we prepare txsHex and pass it here as a parameter so we don't need this part
created_at: 2025-04-19T15:05:43Z
author_association: COLLABORATOR
diff_hunk: @@ -342,7 +230,37 @@ func (m ArcDefaultHandler) POSTTransaction(ctx echo.Context, params api.POSTTran
 	ctx.SetRequest(ctx.Request().WithContext(timeoutCtx))
 	defer cancel()
 
-	postResponse := m.postTransaction(ctx, params)
+	txsHex, err := parseTransactionFromRequest(ctx.Request())
+	if err != nil {
+		e := api.NewErrorFields(api.ErrStatusBadRequest, fmt.Sprintf("error parsing transactions from request: %s", err.Error()))
+		res := PostResponse{e.Status, e}
+		return ctx.JSON(res.StatusCode, res.response)
+	}
+	txsParams := api.POSTTransactionsParams(params)
+	postResponse := m.postTransactions(ctx, txsHex, txsParams)
+
+	// postTransactions returns different PostResponse, until we merge api endpoints we will need this casting
path: internal/api/handler/default.go
user: login: boecklim
body: Let's remove this part `, until we merge api endpoints we will need this casting` as it is not planned at the moment to merge the 2 post api endpoints
created_at: 2025-04-22T12:28:36Z
author_association: COLLABORATOR

PR comments:
user: login: sonarqubecloud[bot]
created_at: 2025-04-22T13:55:44Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=bitcoin-sv_arc&pullRequest=850) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [1 New issue](https://sonarcloud.io/project/issues?id=bitcoin-sv_arc&pullRequest=850&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=bitcoin-sv_arc&pullRequest=850&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=bitcoin-sv_arc&pullRequest=850&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [100.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=bitcoin-sv_arc&pullRequest=850&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=bitcoin-sv_arc&pullRequest=850&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=bitcoin-sv_arc&pullRequest=850)


author_association: NONE