diff --git a/actions/v2/transactions/internal/mapping/annotatedtx/annotated_tx_request_to_engine.go b/actions/v2/transactions/internal/mapping/annotatedtx/annotated_tx_request_to_engine.go index 1813f7042..8411baa15 100644 --- a/actions/v2/transactions/internal/mapping/annotatedtx/annotated_tx_request_to_engine.go +++ b/actions/v2/transactions/internal/mapping/annotatedtx/annotated_tx_request_to_engine.go @@ -3,6 +3,7 @@ package annotatedtx import ( "maps" + "github.com/bitcoin-sv/spv-wallet/engine/v2/bsv" "github.com/bitcoin-sv/spv-wallet/engine/v2/transaction" "github.com/bitcoin-sv/spv-wallet/engine/v2/transaction/outlines" model "github.com/bitcoin-sv/spv-wallet/models/transaction" @@ -14,7 +15,7 @@ type Request model.AnnotatedTransaction // ToEngine converts a request model to the engine model. func (req Request) ToEngine() *outlines.Transaction { return &outlines.Transaction{ - BEEF: req.BEEF, + Hex: bsv.TxHex(req.Hex), Annotations: transaction.Annotations{ Outputs: maps.Collect(func(yield func(int, *transaction.OutputAnnotation) bool) { if req.Annotations == nil || len(req.Annotations.Outputs) == 0 { diff --git a/actions/v2/transactions/internal/mapping/outline/outline_to_response.go b/actions/v2/transactions/internal/mapping/outline/outline_to_response.go index d02c051ae..f0b4f0cdb 100644 --- a/actions/v2/transactions/internal/mapping/outline/outline_to_response.go +++ b/actions/v2/transactions/internal/mapping/outline/outline_to_response.go @@ -9,7 +9,9 @@ import ( // ToResponse converts a transaction outline to a response model. func ToResponse(tx *outlines.Transaction) (*model.AnnotatedTransaction, error) { - res := &model.AnnotatedTransaction{} + res := &model.AnnotatedTransaction{ + Format: tx.Hex.Format(), + } err := mapstructure.Decode(tx, res) if err != nil { return nil, spverrors.ErrCannotMapFromModel.Wrap(err) diff --git a/actions/v2/transactions/internal/mapping/transaction_outline.go b/actions/v2/transactions/internal/mapping/transaction_outline.go index bdef63d75..e8a3526f2 100644 --- a/actions/v2/transactions/internal/mapping/transaction_outline.go +++ b/actions/v2/transactions/internal/mapping/transaction_outline.go @@ -3,6 +3,7 @@ package mapping import ( "maps" + "github.com/bitcoin-sv/spv-wallet/engine/v2/bsv" "github.com/bitcoin-sv/spv-wallet/engine/v2/transaction" "github.com/bitcoin-sv/spv-wallet/engine/v2/transaction/outlines" "github.com/bitcoin-sv/spv-wallet/models/request" @@ -11,7 +12,7 @@ import ( // TransactionOutline maps request's AnnotatedTransaction to outlines.Transaction. func TransactionOutline(req *request.AnnotatedTransaction) *outlines.Transaction { return &outlines.Transaction{ - BEEF: req.BEEF, + Hex: bsv.TxHex(req.Hex), Annotations: transaction.Annotations{ Outputs: maps.Collect(func(yield func(int, *transaction.OutputAnnotation) bool) { if req.Annotations == nil || len(req.Annotations.Outputs) == 0 { diff --git a/actions/v2/transactions/outlines.go b/actions/v2/transactions/outlines.go index 5358338f9..a76dc6f0b 100644 --- a/actions/v2/transactions/outlines.go +++ b/actions/v2/transactions/outlines.go @@ -31,7 +31,7 @@ func transactionOutlines(c *gin.Context, userCtx *reqctx.UserContext) { return } - txOutline, err := reqctx.Engine(c).TransactionOutlinesService().Create(c, spec) + txOutline, err := reqctx.Engine(c).TransactionOutlinesService().CreateBEEF(c, spec) if err != nil { spverrors.ErrorResponse(c, err, logger) return diff --git a/actions/v2/transactions/outlines_endpoint_test.go b/actions/v2/transactions/outlines_endpoint_test.go index 6e74947c4..41dd7c0b3 100644 --- a/actions/v2/transactions/outlines_endpoint_test.go +++ b/actions/v2/transactions/outlines_endpoint_test.go @@ -27,7 +27,8 @@ func TestPOSTTransactionOutlines(t *testing.T) { ] }`, response: `{ - "beef": "0100beef000100000000000100000000000000000e006a04736f6d65012004646174610000000000", + "hex": "0100beef000100000000000100000000000000000e006a04736f6d65012004646174610000000000", + "format": "BEEF", "annotations": { "outputs": { "0": { @@ -48,7 +49,8 @@ func TestPOSTTransactionOutlines(t *testing.T) { ] }`, response: `{ - "beef": "0100beef000100000000000100000000000000000e006a04736f6d65012004646174610000000000", + "hex": "0100beef000100000000000100000000000000000e006a04736f6d65012004646174610000000000", + "format": "BEEF", "annotations": { "outputs": { "0": { @@ -69,7 +71,8 @@ func TestPOSTTransactionOutlines(t *testing.T) { ] }`, response: `{ - "beef": "0100beef000100000000000100000000000000000e006a04736f6d65012004646174610000000000", + "hex": "0100beef000100000000000100000000000000000e006a04736f6d65012004646174610000000000", + "format": "BEEF", "annotations": { "outputs": { "0": { @@ -90,7 +93,8 @@ func TestPOSTTransactionOutlines(t *testing.T) { ] }`, fixtures.RecipientExternal.DefaultPaymail()), response: fmt.Sprintf(`{ - "beef": "0100beef0001000000000001e8030000000000001976a9143e2d1d795f8acaa7957045cc59376177eb04a3c588ac0000000000", + "hex": "0100beef0001000000000001e8030000000000001976a9143e2d1d795f8acaa7957045cc59376177eb04a3c588ac0000000000", + "format": "BEEF", "annotations": { "outputs": { "0": { @@ -122,7 +126,8 @@ func TestPOSTTransactionOutlines(t *testing.T) { fixtures.Sender.DefaultPaymail(), ), response: fmt.Sprintf(`{ - "beef": "0100beef0001000000000001e8030000000000001976a9143e2d1d795f8acaa7957045cc59376177eb04a3c588ac0000000000", + "hex": "0100beef0001000000000001e8030000000000001976a9143e2d1d795f8acaa7957045cc59376177eb04a3c588ac0000000000", + "format": "BEEF", "annotations": { "outputs": { "0": { @@ -158,7 +163,8 @@ func TestPOSTTransactionOutlines(t *testing.T) { fixtures.Sender.DefaultPaymail(), ), response: fmt.Sprintf(`{ - "beef": "0100beef0001000000000002e8030000000000001976a9143e2d1d795f8acaa7957045cc59376177eb04a3c588ac00000000000000000e006a04736f6d65012004646174610000000000", + "hex": "0100beef0001000000000002e8030000000000001976a9143e2d1d795f8acaa7957045cc59376177eb04a3c588ac00000000000000000e006a04736f6d65012004646174610000000000", + "format": "BEEF", "annotations": { "outputs": { "0": { diff --git a/actions/v2/transactions/outlines_record_test.go b/actions/v2/transactions/outlines_record_test.go index 7138a9d14..492aa7675 100644 --- a/actions/v2/transactions/outlines_record_test.go +++ b/actions/v2/transactions/outlines_record_test.go @@ -47,7 +47,7 @@ func TestOutlinesRecordOpReturn(t *testing.T) { // and: request := `{ - "beef": "` + txSpec.BEEF() + `", + "hex": "` + txSpec.BEEF() + `", "annotations": { "outputs": { "0": { @@ -132,7 +132,7 @@ func TestOutlinesRecordOpReturnErrorCases(t *testing.T) { }{ "RecordTransactionOutline for not signed transaction": { request: `{ - "beef": "` + givenUnsignedTX.BEEF() + `" + "hex": "` + givenUnsignedTX.BEEF() + `" }`, expectHttpCode: 400, expectedErr: apierror.ExpectedJSON("error-transaction-validation", "transaction validation failed"), @@ -160,7 +160,7 @@ func TestOutlinesRecordOpReturnErrorCases(t *testing.T) { }, "no-op_return output annotated as data": { request: `{ - "beef": "` + givenTxWithP2PKHOutput.BEEF() + `", + "hex": "` + givenTxWithP2PKHOutput.BEEF() + `", "annotations": { "outputs": { "0": { @@ -207,7 +207,7 @@ func TestOutlinesRecordOpReturnOnBroadcastError(t *testing.T) { // and: txSpec := givenTXWithOpReturn(t) request := `{ - "beef": "` + txSpec.BEEF() + `", + "hex": "` + txSpec.BEEF() + `", "annotations": { "outputs": { "0": { diff --git a/engine/v2/bsv/transaction_hex.go b/engine/v2/bsv/transaction_hex.go new file mode 100644 index 000000000..61f79e821 --- /dev/null +++ b/engine/v2/bsv/transaction_hex.go @@ -0,0 +1,37 @@ +package bsv + +import ( + "strings" + + "github.com/bitcoin-sv/go-sdk/transaction" + "github.com/bitcoin-sv/spv-wallet/engine/spverrors" +) + +// TxHex is a hex representation of a transaction. +type TxHex string + +// IsBEEF checks if the transaction hex is a BEEF hex. +func (h TxHex) IsBEEF() bool { + return strings.HasPrefix(string(h), "0100BEEF") || strings.HasPrefix(string(h), "0100beef") +} + +// IsRawTx checks if the transaction hex is a raw transaction hex. +func (h TxHex) IsRawTx() bool { + return !h.IsBEEF() +} + +// ToBEEFTransaction converts the transaction hex to a BEEF transaction. +func (h TxHex) ToBEEFTransaction() (*transaction.Transaction, error) { + if !h.IsBEEF() { + return nil, spverrors.Newf("transaction hex is not a BEEF hex") + } + return transaction.NewTransactionFromBEEFHex(string(h)) +} + +// Format returns the name of the format of the transaction hex. +func (h TxHex) Format() string { + if h.IsBEEF() { + return "BEEF" + } + return "RAW" +} diff --git a/engine/v2/transaction/outlines/create_op_return_outline_test.go b/engine/v2/transaction/outlines/create_op_return_outline_test.go index c177cbc72..fc7a4693b 100644 --- a/engine/v2/transaction/outlines/create_op_return_outline_test.go +++ b/engine/v2/transaction/outlines/create_op_return_outline_test.go @@ -66,7 +66,7 @@ func TestCreateOpReturnTransactionOutline(t *testing.T) { } // when: - tx, err := service.Create(context.Background(), spec) + tx, err := service.CreateBEEF(context.Background(), spec) // then: thenTx := then.Created(tx).WithNoError(err).WithParseableBEEFHex() @@ -130,7 +130,7 @@ func TestCreateOpReturnTransactionOutline(t *testing.T) { } // when: - tx, err := service.Create(context.Background(), spec) + tx, err := service.CreateBEEF(context.Background(), spec) // then: then.Created(tx).WithError(err).ThatIs(test.expectedError) diff --git a/engine/v2/transaction/outlines/create_outline_test.go b/engine/v2/transaction/outlines/create_outline_test.go index 26d45d363..1245de456 100644 --- a/engine/v2/transaction/outlines/create_outline_test.go +++ b/engine/v2/transaction/outlines/create_outline_test.go @@ -11,7 +11,7 @@ import ( "github.com/bitcoin-sv/spv-wallet/models" ) -func TestCreateTransactionOutlineError(t *testing.T) { +func TestCreateBEEFTransactionOutlineError(t *testing.T) { errorTests := map[string]struct { spec *outlines.TransactionSpec expectedError models.SPVError @@ -44,7 +44,48 @@ func TestCreateTransactionOutlineError(t *testing.T) { service := given.NewTransactionOutlinesService() // when: - tx, err := service.Create(context.Background(), test.spec) + tx, err := service.CreateBEEF(context.Background(), test.spec) + + // then: + then.Created(tx).WithError(err).ThatIs(test.expectedError) + }) + } +} + +func TestCreateRawTransactionOutlineError(t *testing.T) { + errorTests := map[string]struct { + spec *outlines.TransactionSpec + expectedError models.SPVError + }{ + "return error for nil as transaction spec": { + spec: nil, + expectedError: txerrors.ErrTxOutlineSpecificationRequired, + }, + "return error for transaction spec without xPub Id": { + spec: &outlines.TransactionSpec{}, + expectedError: txerrors.ErrTxOutlineSpecificationUserIDRequired, + }, + "return error for no outputs in transaction spec": { + spec: &outlines.TransactionSpec{UserID: fixtures.Sender.ID()}, + expectedError: txerrors.ErrTxOutlineRequiresAtLeastOneOutput, + }, + "return error for empty output list in transaction spec": { + spec: &outlines.TransactionSpec{ + UserID: fixtures.Sender.ID(), + Outputs: outlines.NewOutputsSpecs(), + }, + expectedError: txerrors.ErrTxOutlineRequiresAtLeastOneOutput, + }, + } + for name, test := range errorTests { + t.Run(name, func(t *testing.T) { + given, then := testabilities.New(t) + + // given: + service := given.NewTransactionOutlinesService() + + // when: + tx, err := service.CreateBEEF(context.Background(), test.spec) // then: then.Created(tx).WithError(err).ThatIs(test.expectedError) diff --git a/engine/v2/transaction/outlines/create_paymail_outline_test.go b/engine/v2/transaction/outlines/create_paymail_outline_test.go index e28c6daa4..700b1b015 100644 --- a/engine/v2/transaction/outlines/create_paymail_outline_test.go +++ b/engine/v2/transaction/outlines/create_paymail_outline_test.go @@ -41,7 +41,7 @@ func TestCreatePaymailTransactionOutline(t *testing.T) { } // when: - tx, err := service.Create(context.Background(), spec) + tx, err := service.CreateBEEF(context.Background(), spec) // then: thenTx := then.Created(tx).WithNoError(err).WithParseableBEEFHex() @@ -83,7 +83,7 @@ func TestCreatePaymailTransactionOutline(t *testing.T) { } // when: - tx, err := service.Create(context.Background(), spec) + tx, err := service.CreateBEEF(context.Background(), spec) // then: thenTx := then.Created(tx).WithNoError(err).WithParseableBEEFHex() @@ -128,7 +128,7 @@ func TestCreatePaymailTransactionOutline(t *testing.T) { } // when: - tx, err := service.Create(context.Background(), spec) + tx, err := service.CreateBEEF(context.Background(), spec) // then: then.Created(tx).WithNoError(err).WithParseableBEEFHex(). @@ -276,7 +276,7 @@ func TestCreatePaymailTransactionOutline(t *testing.T) { } // when: - tx, err := service.Create(context.Background(), spec) + tx, err := service.CreateBEEF(context.Background(), spec) // then: then.Created(tx).WithError(err).ThatIs(test.expectedError) @@ -344,7 +344,7 @@ func TestCreatePaymailTransactionOutline(t *testing.T) { } // when: - tx, err := service.Create(context.Background(), spec) + tx, err := service.CreateBEEF(context.Background(), spec) // then: then.Created(tx).WithError(err).ThatIs(test.expectedError) diff --git a/engine/v2/transaction/outlines/interface.go b/engine/v2/transaction/outlines/interface.go index e71efb18f..e8dc30bfd 100644 --- a/engine/v2/transaction/outlines/interface.go +++ b/engine/v2/transaction/outlines/interface.go @@ -3,6 +3,7 @@ package outlines import ( "context" + "github.com/bitcoin-sv/spv-wallet/engine/v2/bsv" "github.com/bitcoin-sv/spv-wallet/engine/v2/transaction" ) @@ -14,11 +15,11 @@ type PaymailAddressService interface { // Service is a service for creating transaction outlines. type Service interface { - Create(ctx context.Context, spec *TransactionSpec) (*Transaction, error) + CreateBEEF(ctx context.Context, spec *TransactionSpec) (*Transaction, error) } // Transaction represents a transaction outline. type Transaction struct { - BEEF string + Hex bsv.TxHex Annotations transaction.Annotations } diff --git a/engine/v2/transaction/outlines/testabilities/assert_outline_transaction.go b/engine/v2/transaction/outlines/testabilities/assert_outline_transaction.go index e7710b2fd..8ca0d4720 100644 --- a/engine/v2/transaction/outlines/testabilities/assert_outline_transaction.go +++ b/engine/v2/transaction/outlines/testabilities/assert_outline_transaction.go @@ -65,7 +65,7 @@ func (a *assertion) ThatIs(expectedError error) { a.assert.ErrorIs(a.err, expectedError) } -// WithNoError checks if there was no error and result is not nil. It also checks if BEEF hex is parseable. +// WithNoError checks if there was no error and result is not nil. func (a *assertion) WithNoError(err error) SuccessfullyCreatedTransactionOutlineAssertion { a.t.Helper() a.require.NoError(err, "Creation of transaction outline has finished with error") @@ -75,11 +75,11 @@ func (a *assertion) WithNoError(err error) SuccessfullyCreatedTransactionOutline func (a *assertion) WithParseableBEEFHex() WithParseableBEEFTransactionOutlineAssertion { a.t.Helper() - a.t.Logf("BEEF: %s", a.txOutline.BEEF) + a.t.Logf("Hex: %s", a.txOutline.Hex) var err error - a.tx, err = sdk.NewTransactionFromBEEFHex(a.txOutline.BEEF) - a.require.NoErrorf(err, "Invalid BEEF hex: %s", a.txOutline.BEEF) + a.tx, err = a.txOutline.Hex.ToBEEFTransaction() + a.require.NoErrorf(err, "Invalid Hex hex: %s", a.txOutline.Hex) return a } diff --git a/engine/v2/transaction/outlines/transaction_outlines_service.go b/engine/v2/transaction/outlines/transaction_outlines_service.go index df1c45f5a..c8c306bd1 100644 --- a/engine/v2/transaction/outlines/transaction_outlines_service.go +++ b/engine/v2/transaction/outlines/transaction_outlines_service.go @@ -5,6 +5,7 @@ import ( "github.com/bitcoin-sv/spv-wallet/engine/paymail" "github.com/bitcoin-sv/spv-wallet/engine/spverrors" + "github.com/bitcoin-sv/spv-wallet/engine/v2/bsv" "github.com/bitcoin-sv/spv-wallet/engine/v2/transaction/errors" "github.com/rs/zerolog" ) @@ -32,8 +33,8 @@ func NewService(paymailService paymail.ServiceClient, paymailAddressService Paym } } -// Create creates a new transaction outline based on specification. -func (s *service) Create(ctx context.Context, spec *TransactionSpec) (*Transaction, error) { +// CreateBEEF creates a new transaction outline based on specification. +func (s *service) CreateBEEF(ctx context.Context, spec *TransactionSpec) (*Transaction, error) { if spec == nil { return nil, txerrors.ErrTxOutlineSpecificationRequired } @@ -61,7 +62,7 @@ func (s *service) Create(ctx context.Context, spec *TransactionSpec) (*Transacti } return &Transaction{ - BEEF: beef, + Hex: bsv.TxHex(beef), Annotations: annotations, }, nil } diff --git a/engine/v2/transaction/record/record_outline.go b/engine/v2/transaction/record/record_outline.go index 7b4419541..733cc9c50 100644 --- a/engine/v2/transaction/record/record_outline.go +++ b/engine/v2/transaction/record/record_outline.go @@ -3,7 +3,7 @@ package record import ( "context" - trx "github.com/bitcoin-sv/go-sdk/transaction" + "github.com/bitcoin-sv/spv-wallet/engine/spverrors" "github.com/bitcoin-sv/spv-wallet/engine/v2/transaction/errors" "github.com/bitcoin-sv/spv-wallet/engine/v2/transaction/outlines" "github.com/bitcoin-sv/spv-wallet/engine/v2/transaction/txmodels" @@ -11,7 +11,11 @@ import ( // RecordTransactionOutline will validate, broadcast and save a transaction outline func (s *Service) RecordTransactionOutline(ctx context.Context, userID string, outline *outlines.Transaction) (*txmodels.RecordedOutline, error) { - tx, err := trx.NewTransactionFromBEEFHex(outline.BEEF) + if outline.Hex.IsRawTx() { + return nil, spverrors.Newf("not implemented recording outline with raw transaction") + } + + tx, err := outline.Hex.ToBEEFTransaction() if err != nil { return nil, txerrors.ErrTxValidation.Wrap(err) } diff --git a/models/transaction/annotated_transaction.go b/models/transaction/annotated_transaction.go index de02a5033..7e0c920c1 100644 --- a/models/transaction/annotated_transaction.go +++ b/models/transaction/annotated_transaction.go @@ -7,8 +7,10 @@ import ( // AnnotatedTransaction represents a transaction with annotations. type AnnotatedTransaction struct { - // BEEF is the transaction hex in BEEF format. - BEEF string `json:"beef"` + // Hex is the transaction in binary format specified by type. + Hex string `json:"hex"` + // Format is the format of the transaction hex ex. BEEF, RAW. + Format string `json:"format"` // Annotations is the metadata for the transaction. Annotations *Annotations `json:"annotations"` }