Skip to content
This repository has been archived by the owner on Oct 21, 2024. It is now read-only.

Commit

Permalink
feat(BUX-156): add SubmitBatchTransactions
Browse files Browse the repository at this point in the history
  • Loading branch information
arkadiuszos4chain committed Aug 8, 2023
1 parent 0063bb2 commit 57471bd
Show file tree
Hide file tree
Showing 7 changed files with 208 additions and 18 deletions.
35 changes: 35 additions & 0 deletions broadcast/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package broadcast

import (
"errors"
"fmt"
"strings"
)

var ErrClientUndefined = errors.New("client is undefined")
Expand All @@ -17,3 +19,36 @@ var ErrUnableToDecodeResponse = errors.New("unable to decode response")
var ErrMissingStatus = errors.New("missing tx status")

var ErrStrategyUnkown = errors.New("unknown strategy")

type ArcError struct {
Type string `json:"type"`
Title string `json:"title"`
Status int `json:"status"`
Detail string `json:"detail"`
Instance string `json:"instance,omitempty"`
Txid string `json:"txid,omitempty"`
ExtraInfo string `json:"extraInfo,omitempty"`
}

func (err ArcError) Error() string {
sb := strings.Builder{}

sb.WriteString("arc error: {")
sb.WriteString(fmt.Sprintf("type: %s, title: %s, status: %d, detail: %s",
err.Type, err.Title, err.Status, err.Detail))

if err.Instance != "" {
sb.Write([]byte(fmt.Sprintf(", instance: %s", err.Instance)))
}

if err.Txid != "" {
sb.Write([]byte(fmt.Sprintf(", txid: %s", err.Txid)))
}

if err.ExtraInfo != "" {
sb.Write([]byte(fmt.Sprintf(", extraInfo: %s", err.ExtraInfo)))
}

sb.WriteString("}")
return sb.String()
}
1 change: 1 addition & 0 deletions broadcast/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ type TransactionSubmitter interface {
}

type TransactionsSubmitter interface {
SubmitBatchTransactions(ctx context.Context, tx []*Transaction) ([]*SubmitTxResponse, error)
}

type Client interface {
Expand Down
8 changes: 4 additions & 4 deletions broadcast/internal/arc/arc_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ import (
)

const (
arcQueryTxRoute = "/v1/tx/"
arcPolicyQuoteRoute = "/v1/policy"
arcSubmitTxRoute = "/v1/tx"
arcSubmitTxsRoute = "/v1/txs"
arcQueryTxRoute = "/v1/tx/"
arcPolicyQuoteRoute = "/v1/policy"
arcSubmitTxRoute = "/v1/tx"
arcSubmitBatchTxsRoute = "/v1/txs"
)

type Config interface {
Expand Down
119 changes: 107 additions & 12 deletions broadcast/internal/arc/arc_submit_tx.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,18 @@ import (
"context"
"encoding/json"
"errors"
"io"
"net/http"
"strconv"

"github.com/bitcoin-sv/go-broadcast-client/broadcast"
arc_utils "github.com/bitcoin-sv/go-broadcast-client/broadcast/internal/arc/utils"
"github.com/bitcoin-sv/go-broadcast-client/broadcast/internal/httpclient"
)

type SubmitTxRequest struct {
RawTx string `json:"rawTx"`
}

var ErrSubmitTxMarshal = errors.New("error while marshalling submit tx body")

func (a *ArcClient) SubmitTransaction(ctx context.Context, tx *broadcast.Transaction) (*broadcast.SubmitTxResponse, error) {
Expand All @@ -30,6 +35,27 @@ func (a *ArcClient) SubmitTransaction(ctx context.Context, tx *broadcast.Transac
return result, nil
}

func (a *ArcClient) SubmitBatchTransactions(ctx context.Context, txs []*broadcast.Transaction) ([]*broadcast.SubmitTxResponse, error) {
if a == nil {
return nil, broadcast.ErrClientUndefined
}

if txs == nil || len(txs) == 0 {
return nil, errors.New("invalid request, no transactions to submit")
}

result, err := submitBatchTransactions(ctx, a, txs)
if err != nil {
return nil, err
}

if err := validateBatchResponse(result); err != nil {
return nil, err
}

return result, nil
}

func submitTransaction(ctx context.Context, arc *ArcClient, tx *broadcast.Transaction) (*broadcast.SubmitTxResponse, error) {
url := arc.apiURL + arcSubmitTxRoute
data, err := createSubmitTxBody(tx)
Expand All @@ -49,11 +75,44 @@ func submitTransaction(ctx context.Context, arc *ArcClient, tx *broadcast.Transa
ctx,
pld,
)
if err != nil {
return nil, handleHttpError(resp, err)
}

model := broadcast.SubmitTxResponse{}
err = arc_utils.DecodeResponseBody(resp.Body, &model)
if err != nil {
return nil, err
}

model, err := decodeSubmitTxBody(resp.Body)
return &model, nil
}

func submitBatchTransactions(ctx context.Context, arc *ArcClient, txs []*broadcast.Transaction) ([]*broadcast.SubmitTxResponse, error) {
url := arc.apiURL + arcSubmitBatchTxsRoute
data, err := createSubmitBatchTxsBody(txs)
if err != nil {
return nil, err
}

pld := httpclient.NewPayload(
httpclient.POST,
url,
arc.token,
data,
)
appendSubmitTxHeaders(&pld, txs[0])

resp, err := arc.HTTPClient.DoRequest(
ctx,
pld,
)
if err != nil {
return nil, handleHttpError(resp, err)
}

var model []*broadcast.SubmitTxResponse
err = arc_utils.DecodeResponseBody(resp.Body, &model)
if err != nil {
return nil, err
}
Expand All @@ -62,10 +121,23 @@ func submitTransaction(ctx context.Context, arc *ArcClient, tx *broadcast.Transa
}

func createSubmitTxBody(tx *broadcast.Transaction) ([]byte, error) {
body := map[string]string{
"rawTx": tx.RawTx,
}
body := &SubmitTxRequest{tx.RawTx}
data, err := json.Marshal(body)

if err != nil {
return nil, ErrSubmitTxMarshal
}

return data, nil
}

func createSubmitBatchTxsBody(txs []*broadcast.Transaction) ([]byte, error) {
rawTxs := make([]*SubmitTxRequest, 0, len(txs))
for _, tx := range txs {
rawTxs = append(rawTxs, &SubmitTxRequest{RawTx: tx.RawTx})
}

data, err := json.Marshal(rawTxs)
if err != nil {
return nil, ErrSubmitTxMarshal
}
Expand All @@ -91,15 +163,14 @@ func appendSubmitTxHeaders(pld *httpclient.HTTPRequest, tx *broadcast.Transactio
}
}

func decodeSubmitTxBody(body io.ReadCloser) (*broadcast.SubmitTxResponse, error) {
model := broadcast.SubmitTxResponse{}
err := json.NewDecoder(body).Decode(&model)

if err != nil {
return nil, broadcast.ErrUnableToDecodeResponse
func validateBatchResponse(model []*broadcast.SubmitTxResponse) error {
for _, tx := range model {
if err := validateSubmitTxResponse(tx); err != nil {
return err
}
}

return &model, nil
return nil
}

func validateSubmitTxResponse(model *broadcast.SubmitTxResponse) error {
Expand All @@ -109,3 +180,27 @@ func validateSubmitTxResponse(model *broadcast.SubmitTxResponse) error {

return nil
}

func handleHttpError(response *http.Response, httpClientError error) error {
if response != nil { // client respond with code different than 2xx
var err error

switch response.StatusCode {
case 400:
err = arc_utils.DecodeArcError(response.Body)
case 422: // Unprocessable entity - with IETF RFC 7807 Error object
err = arc_utils.DecodeArcError(response.Body)
case 465: // Fee too low
err = arc_utils.DecodeArcError(response.Body)
case 466: // Conflicting transaction found
err = arc_utils.DecodeArcError(response.Body)

default:
err = errors.New(response.Status)
}

return err
}

return httpClientError // http client internal error
}
28 changes: 28 additions & 0 deletions broadcast/internal/arc/utils/arc_utils.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package arc_utils

import (
"encoding/json"
"io"

"github.com/bitcoin-sv/go-broadcast-client/broadcast"
)

func DecodeArcError(body io.ReadCloser) error {
resultError := broadcast.ArcError{}
err := json.NewDecoder(body).Decode(&resultError)

if err != nil {
return broadcast.ErrUnableToDecodeResponse
}

return resultError
}

func DecodeResponseBody(body io.ReadCloser, resultOutput any) error {
err := json.NewDecoder(body).Decode(resultOutput)
if err != nil {
return broadcast.ErrUnableToDecodeResponse
}

return nil
}
24 changes: 23 additions & 1 deletion broadcast/internal/composite/broadcaster.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,33 @@ func (c *compositeBroadcaster) SubmitTransaction(ctx context.Context, tx *broadc
return nil, err
}

// Convert result to QueryTxResponse
// Convert result to SubmitTxResponse
submitTxResponse, ok := result.(*broadcast.SubmitTxResponse)
if !ok {
return nil, fmt.Errorf("unexpected result type: %T", result)
}

return submitTxResponse, nil
}

func (c *compositeBroadcaster) SubmitBatchTransactions(ctx context.Context, txs []*broadcast.Transaction) ([]*broadcast.SubmitTxResponse, error) {
executionFuncs := make([]executionFunc, len(c.broadcasters))
for i, broadcaster := range c.broadcasters {
executionFuncs[i] = func(ctx context.Context) (Result, error) {
return broadcaster.SubmitBatchTransactions(ctx, txs)
}
}

result, err := c.strategy.Execute(ctx, executionFuncs)
if err != nil {
return nil, err
}

// Convert result to []SubmitTxResponse
submitTxResponse, ok := result.([]*broadcast.SubmitTxResponse)
if !ok {
return nil, fmt.Errorf("unexpected result type: %T", result)
}

return submitTxResponse, nil
}
11 changes: 10 additions & 1 deletion broadcast/internal/httpclient/http_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package httpclient
import (
"bytes"
"context"
"errors"
"io"
"net/http"

Expand Down Expand Up @@ -84,5 +85,13 @@ func (hc *HTTPClient) DoRequest(ctx context.Context, pld HTTPRequest) (*http.Res
return nil, err
}

return resp, nil
if hasSuccessCode(resp) {
return resp, nil
}

return resp, errors.New("server responded with no-success code")
}

func hasSuccessCode(resp *http.Response) bool {
return resp.StatusCode >= 200 && resp.StatusCode < 300
}

0 comments on commit 57471bd

Please sign in to comment.