From bfde677e143d36b04b3dda363bcb3678aa12fa17 Mon Sep 17 00:00:00 2001 From: Damian Orzepowski Date: Wed, 22 Jan 2025 14:05:30 +0100 Subject: [PATCH] refactor(SPV-1404): adjust tx outline to type42 changes. --- .../outline/outline_request_to_engine.go | 13 ++- actions/transactions/outlines.go | 8 +- .../transactions/outlines_endpoint_test.go | 39 +++++++ engine/client_internal.go | 31 +----- engine/database/repository/paymails.go | 17 +++ ...ress_service_interface.go => interface.go} | 4 +- .../paymailaddress/paymail_address_service.go | 45 ++++---- .../ability_paymail_address_service.go | 67 ------------ engine/tester/fixtures/users_fixtures.go | 3 + engine/transaction/annotation.go | 14 ++- engine/transaction/errors/errors.go | 7 +- ...st.go => create_op_return_outline_test.go} | 31 +++--- ...e_draft_test.go => create_outline_test.go} | 9 +- ...test.go => create_paymail_outline_test.go} | 51 +++++---- .../context.go => evaluation_context.go} | 26 +++-- engine/transaction/outlines/inteface.go | 8 -- engine/transaction/outlines/interface.go | 24 +++++ .../outlines/internal/evaluation/interface.go | 18 ---- .../op_return.go => output_op_return.go} | 5 +- .../{outputs/paymail.go => output_paymail.go} | 17 ++- .../spec.go => output_specifications.go} | 35 +++--- ...tion.go => ability_outline_transaction.go} | 0 .../testabilities/assert_outline_input.go | 31 ++++++ .../testabilities/assert_outline_output.go | 79 ++++++++++++++ ...ction.go => assert_outline_transaction.go} | 101 +++++++----------- ...tion.go => fixture_outline_transaction.go} | 11 +- .../mock_paymail_address_service.go | 42 ++++++++ engine/transaction/outlines/transaction.go | 9 -- .../outlines/transaction_outlines_service.go | 25 ++--- .../transaction/outlines/transaction_spec.go | 26 ++--- .../internal/sql}/inputs_query_composer.go | 2 +- .../inputs => utxo/internal/sql}/selector.go | 10 +- .../internal/sql}/selector_example_test.go | 20 ++-- .../internal/sql}/selector_test.go | 4 +- .../assertions_inputs_selector.go | 0 .../testabilities/fixture_inputs_selector.go | 8 +- .../testability_inputs_selector.go | 0 37 files changed, 466 insertions(+), 374 deletions(-) rename engine/paymailaddress/{paymail_address_service_interface.go => interface.go} (59%) delete mode 100644 engine/paymailaddress/testabilities/ability_paymail_address_service.go rename engine/transaction/outlines/{create_op_return_draft_test.go => create_op_return_outline_test.go} (85%) rename engine/transaction/outlines/{create_draft_test.go => create_outline_test.go} (82%) rename engine/transaction/outlines/{create_paymail_draft_test.go => create_paymail_outline_test.go} (91%) rename engine/transaction/outlines/{internal/evaluation/context.go => evaluation_context.go} (53%) delete mode 100644 engine/transaction/outlines/inteface.go create mode 100644 engine/transaction/outlines/interface.go delete mode 100644 engine/transaction/outlines/internal/evaluation/interface.go rename engine/transaction/outlines/{outputs/op_return.go => output_op_return.go} (90%) rename engine/transaction/outlines/{outputs/paymail.go => output_paymail.go} (85%) rename engine/transaction/outlines/{outputs/spec.go => output_specifications.go} (69%) rename engine/transaction/outlines/testabilities/{ability_draft_transaction.go => ability_outline_transaction.go} (100%) create mode 100644 engine/transaction/outlines/testabilities/assert_outline_input.go create mode 100644 engine/transaction/outlines/testabilities/assert_outline_output.go rename engine/transaction/outlines/testabilities/{assert_draft_transaction.go => assert_outline_transaction.go} (51%) rename engine/transaction/outlines/testabilities/{fixture_draft_transaction.go => fixture_outline_transaction.go} (77%) create mode 100644 engine/transaction/outlines/testabilities/mock_paymail_address_service.go delete mode 100644 engine/transaction/outlines/transaction.go rename engine/transaction/outlines/{internal/inputs => utxo/internal/sql}/inputs_query_composer.go (99%) rename engine/transaction/outlines/{internal/inputs => utxo/internal/sql}/selector.go (91%) rename engine/transaction/outlines/{internal/inputs => utxo/internal/sql}/selector_example_test.go (85%) rename engine/transaction/outlines/{internal/inputs => utxo/internal/sql}/selector_test.go (97%) rename engine/transaction/outlines/{internal/inputs => utxo/internal/sql}/testabilities/assertions_inputs_selector.go (100%) rename engine/transaction/outlines/{internal/inputs => utxo/internal/sql}/testabilities/fixture_inputs_selector.go (73%) rename engine/transaction/outlines/{internal/inputs => utxo/internal/sql}/testabilities/testability_inputs_selector.go (100%) diff --git a/actions/transactions/internal/mapping/outline/outline_request_to_engine.go b/actions/transactions/internal/mapping/outline/outline_request_to_engine.go index e543c27b4..9174555b8 100644 --- a/actions/transactions/internal/mapping/outline/outline_request_to_engine.go +++ b/actions/transactions/internal/mapping/outline/outline_request_to_engine.go @@ -6,7 +6,6 @@ import ( "github.com/bitcoin-sv/spv-wallet/engine/spverrors" "github.com/bitcoin-sv/spv-wallet/engine/transaction/outlines" - "github.com/bitcoin-sv/spv-wallet/engine/transaction/outlines/outputs" "github.com/bitcoin-sv/spv-wallet/models/request" "github.com/bitcoin-sv/spv-wallet/models/request/opreturn" paymailreq "github.com/bitcoin-sv/spv-wallet/models/request/paymail" @@ -17,9 +16,9 @@ import ( type Request request.TransactionSpecification // ToEngine converts a transaction outline request model to the engine model. -func (tx Request) ToEngine(xPubID string) (*outlines.TransactionSpec, error) { +func (tx Request) ToEngine(userID string) (*outlines.TransactionSpec, error) { spec := &outlines.TransactionSpec{ - XPubID: xPubID, + UserID: userID, } config := mapstructure.DecoderConfig{ DecodeHook: outputsHookFunc(), @@ -40,7 +39,7 @@ func (tx Request) ToEngine(xPubID string) (*outlines.TransactionSpec, error) { func outputsHookFunc() mapstructure.DecodeHookFunc { return func(_ reflect.Type, to reflect.Type, data interface{}) (interface{}, error) { - specs := outputs.NewSpecifications() + specs := outlines.NewOutputsSpec() reqOutputs, ok := data.([]request.Output) if !ok { return data, nil @@ -60,13 +59,13 @@ func outputsHookFunc() mapstructure.DecodeHookFunc { } } -func outputSpecFromRequest(req request.Output) (outputs.Spec, error) { +func outputSpecFromRequest(req request.Output) (outlines.OutputSpec, error) { switch o := req.(type) { case opreturn.Output: - out := outputs.OpReturn(o) + out := outlines.OpReturn(o) return &out, nil case paymailreq.Output: - out := outputs.Paymail(o) + out := outlines.Paymail(o) return &out, nil default: return nil, errors.New("unsupported output type") diff --git a/actions/transactions/outlines.go b/actions/transactions/outlines.go index a64c81d73..d43a45dad 100644 --- a/actions/transactions/outlines.go +++ b/actions/transactions/outlines.go @@ -19,7 +19,13 @@ func transactionOutlines(c *gin.Context, userCtx *reqctx.UserContext) { return } - spec, err := outline.Request(requestBody).ToEngine(userCtx.GetXPubID()) + userID, err := userCtx.ShouldGetUserID() + if err != nil { + spverrors.ErrorResponse(c, err, logger) + return + } + + spec, err := outline.Request(requestBody).ToEngine(userID) if err != nil { spverrors.ErrorResponse(c, err, logger) return diff --git a/actions/transactions/outlines_endpoint_test.go b/actions/transactions/outlines_endpoint_test.go index 0916a642b..b4c37ca95 100644 --- a/actions/transactions/outlines_endpoint_test.go +++ b/actions/transactions/outlines_endpoint_test.go @@ -140,6 +140,45 @@ func TestPOSTTransactionOutlines(t *testing.T) { fixtures.Sender.DefaultPaymail(), ), }, + "create transaction outline for paymail and data": { + request: fmt.Sprintf(`{ + "outputs": [ + { + "type": "paymail", + "to": "%s", + "satoshis": 1000, + "from": "%s" + }, + { + "type": "op_return", + "data": [ "some", " ", "data" ] + } + ] + }`, fixtures.RecipientExternal.DefaultPaymail(), + fixtures.Sender.DefaultPaymail(), + ), + response: fmt.Sprintf(`{ + "beef": "0100beef0001000000000002e8030000000000001976a9143e2d1d795f8acaa7957045cc59376177eb04a3c588ac00000000000000000e006a04736f6d65012004646174610000000000", + "annotations": { + "outputs": { + "0": { + "bucket": "bsv", + "paymail": { + "receiver": "%s", + "reference": "z0bac4ec-6f15-42de-9ef4-e60bfdabf4f7", + "sender": "%s" + } + }, + "1": { + "bucket": "data" + } + } + } + }`, + fixtures.RecipientExternal.DefaultPaymail(), + fixtures.Sender.DefaultPaymail(), + ), + }, } for name, test := range successTestCases { t.Run(name, func(t *testing.T) { diff --git a/engine/client_internal.go b/engine/client_internal.go index ab6b835be..70d73096c 100644 --- a/engine/client_internal.go +++ b/engine/client_internal.go @@ -166,36 +166,7 @@ func (c *Client) loadPaymailAddressService() error { if c.options.paymailAddressService != nil { return nil } - c.options.paymailAddressService = paymailaddress.NewService( - func(ctx context.Context, address string) (string, error) { - paymailAddress, err := c.GetPaymailAddress(ctx, address) - if err != nil { - return "", err - } - return paymailAddress.XpubID, nil - }, - func(ctx context.Context, xPubId string) ([]string, error) { - page := &datastore.QueryParams{ - PageSize: 10, - OrderByField: createdAtField, - SortDirection: datastore.SortAsc, - } - - conditions := map[string]interface{}{ - xPubIDField: xPubId, - } - - addresses, err := c.GetPaymailAddresses(ctx, nil, conditions, page) - if err != nil { - return nil, err - } - result := make([]string, 0, len(addresses)) - for _, address := range addresses { - result = append(result, address.String()) - } - return result, nil - }, - ) + c.options.paymailAddressService = paymailaddress.NewService(c.Repositories().Paymails) return nil } diff --git a/engine/database/repository/paymails.go b/engine/database/repository/paymails.go index 2d228f12a..85dccdacb 100644 --- a/engine/database/repository/paymails.go +++ b/engine/database/repository/paymails.go @@ -34,3 +34,20 @@ func (p *Paymails) Get(ctx context.Context, alias, domain string) (*database.Pay return &paymail, nil } + +// GetDefault returns a default paymail for user. +func (p *Paymails) GetDefault(ctx context.Context, userID string) (*database.Paymail, error) { + var paymail database.Paymail + if err := p.db. + WithContext(ctx). + Where("user_id = ?", userID). + Order("created_at ASC"). + First(&paymail).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, nil + } + return nil, err + } + + return &paymail, nil +} diff --git a/engine/paymailaddress/paymail_address_service_interface.go b/engine/paymailaddress/interface.go similarity index 59% rename from engine/paymailaddress/paymail_address_service_interface.go rename to engine/paymailaddress/interface.go index b7e4f79ab..a6160f5d8 100644 --- a/engine/paymailaddress/paymail_address_service_interface.go +++ b/engine/paymailaddress/interface.go @@ -4,6 +4,6 @@ import "context" // Service is a component that provides methods for working with paymail address. type Service interface { - HasPaymailAddress(ctx context.Context, xPubID string, address string) (bool, error) - GetDefaultPaymailAddress(ctx context.Context, xPubID string) (string, error) + HasPaymailAddress(ctx context.Context, userID string, address string) (bool, error) + GetDefaultPaymailAddress(ctx context.Context, userID string) (string, error) } diff --git a/engine/paymailaddress/paymail_address_service.go b/engine/paymailaddress/paymail_address_service.go index 87fabb9eb..0023d3843 100644 --- a/engine/paymailaddress/paymail_address_service.go +++ b/engine/paymailaddress/paymail_address_service.go @@ -3,51 +3,44 @@ package paymailaddress import ( "context" + "github.com/bitcoin-sv/go-paymail" + "github.com/bitcoin-sv/spv-wallet/engine/database/repository" "github.com/bitcoin-sv/spv-wallet/engine/paymailaddress/paerrors" "github.com/bitcoin-sv/spv-wallet/engine/spverrors" ) type service struct { - // INFO: this is the first step to slowly move paymail address to a separate package. - getXPubIDByPaymailAddress func(ctx context.Context, paymailAddress string) (string, error) - getPaymailAddressesByXPubIDOrderByCreatedAsc func(ctx context.Context, xPubId string) ([]string, error) + repo *repository.Paymails } // NewService creates a new paymail address service. -func NewService( - getXPubIDByPaymailAddress func(ctx context.Context, paymailAddress string) (string, error), - getPaymailAddressesByXPubIDOrderByCreatedAsc func(ctx context.Context, xPubId string) ([]string, error), -) Service { - if getXPubIDByPaymailAddress == nil { - panic("getXPubIDByPaymailAddress is required to create paymail address service") - } - if getPaymailAddressesByXPubIDOrderByCreatedAsc == nil { - panic("getPaymailAddressesByXPubIDOrderByCreatedAsc is required to create paymail address service") - } - - return &service{ - getXPubIDByPaymailAddress: getXPubIDByPaymailAddress, - getPaymailAddressesByXPubIDOrderByCreatedAsc: getPaymailAddressesByXPubIDOrderByCreatedAsc, - } +func NewService(repo *repository.Paymails) Service { + return &service{repo: repo} } -// HasPaymailAddress checks if the given address belongs to a given xPubId. -func (s *service) HasPaymailAddress(ctx context.Context, xPubID string, address string) (bool, error) { - paymailXpubID, err := s.getXPubIDByPaymailAddress(ctx, address) +// HasPaymailAddress checks if the given address belongs to a given User. +func (s *service) HasPaymailAddress(ctx context.Context, userID string, address string) (bool, error) { + alias, domain, _ := paymail.SanitizePaymail(address) + pm, err := s.repo.Get(ctx, alias, domain) if err != nil { return false, err } - return paymailXpubID == xPubID, nil + + if pm == nil { + return false, nil + } + + return pm.UserID == userID, nil } // GetDefaultPaymailAddress returns the default paymail address for the given xPubId. func (s *service) GetDefaultPaymailAddress(ctx context.Context, xPubID string) (string, error) { - addresses, err := s.getPaymailAddressesByXPubIDOrderByCreatedAsc(ctx, xPubID) + pm, err := s.repo.GetDefault(ctx, xPubID) if err != nil { return "", spverrors.ErrInternal.Wrap(err) - } - if len(addresses) == 0 { + } else if pm == nil { return "", paerrors.ErrNoDefaultPaymailAddress } - return addresses[0], nil + + return pm.Alias + "@" + pm.Domain, nil } diff --git a/engine/paymailaddress/testabilities/ability_paymail_address_service.go b/engine/paymailaddress/testabilities/ability_paymail_address_service.go deleted file mode 100644 index b44d35123..000000000 --- a/engine/paymailaddress/testabilities/ability_paymail_address_service.go +++ /dev/null @@ -1,67 +0,0 @@ -package testabilities - -import ( - "context" - "slices" - "testing" - - "github.com/bitcoin-sv/spv-wallet/engine/paymailaddress" - "github.com/bitcoin-sv/spv-wallet/engine/spverrors" - "github.com/bitcoin-sv/spv-wallet/engine/tester/fixtures" -) - -// PaymailAddressServiceFixture is a test fixture - used for establishing environment for test. -type PaymailAddressServiceFixture interface { - NewPaymailAddressService() paymailaddress.Service -} - -type paymailAddressServiceAbility struct { - t testing.TB - mockRepository *mockRepository -} - -// Given creates a new test fixture. -func Given(t testing.TB) (given PaymailAddressServiceFixture) { - repo := newMockedRepository(t) - ability := &paymailAddressServiceAbility{ - t: t, - mockRepository: repo, - } - - return ability -} - -// NewPaymailAddressService creates a new instance of the paymail address service to use in tests. -func (p *paymailAddressServiceAbility) NewPaymailAddressService() paymailaddress.Service { - return paymailaddress.NewService(p.mockRepository.getXPubIDByPaymailAddress, p.mockRepository.getPaymailAddressesByXPubIDOrderByCreatedAsc) -} - -type mockRepository struct { - t testing.TB - users []fixtures.User -} - -func newMockedRepository(t testing.TB) *mockRepository { - return &mockRepository{ - t: t, - users: fixtures.AllUsers(), - } -} - -func (s *mockRepository) getXPubIDByPaymailAddress(_ context.Context, paymailAddress string) (string, error) { - for _, user := range s.users { - if slices.Contains(user.Paymails, paymailAddress) && user.XPubID() != "" { - return user.XPubID(), nil - } - } - return "", spverrors.ErrCouldNotFindPaymail -} - -func (s *mockRepository) getPaymailAddressesByXPubIDOrderByCreatedAsc(_ context.Context, xPubID string) ([]string, error) { - for _, user := range s.users { - if user.XPubID() == xPubID { - return user.Paymails, nil - } - } - return make([]string, 0), nil -} diff --git a/engine/tester/fixtures/users_fixtures.go b/engine/tester/fixtures/users_fixtures.go index f5bcf70d0..482196c2f 100644 --- a/engine/tester/fixtures/users_fixtures.go +++ b/engine/tester/fixtures/users_fixtures.go @@ -77,6 +77,9 @@ var ( // DefaultPaymail returns the default paymail of this user. func (f *User) DefaultPaymail() string { + if len(f.Paymails) == 0 { + return "" + } return f.Paymails[0] } diff --git a/engine/transaction/annotation.go b/engine/transaction/annotation.go index a290b2212..5f59efd8b 100644 --- a/engine/transaction/annotation.go +++ b/engine/transaction/annotation.go @@ -7,9 +7,20 @@ import ( // Annotations represents a transaction metadata that will be used by server to properly handle given transaction. type Annotations struct { + Inputs InputAnnotations Outputs OutputsAnnotations } +// InputAnnotations represents the metadata for chosen inputs. The key is the index of the input. +type InputAnnotations map[int]*InputAnnotation + +// InputAnnotation represents the metadata for the input. +type InputAnnotation struct { +} + +// OutputsAnnotations represents the metadata for chosen outputs. The key is the index of the output. +type OutputsAnnotations map[int]*OutputAnnotation + // OutputAnnotation represents the metadata for the output. type OutputAnnotation struct { // What type of bucket should this output be stored in. @@ -21,9 +32,6 @@ type OutputAnnotation struct { // PaymailAnnotation is the metadata for the paymail output. type PaymailAnnotation transaction.PaymailAnnotation -// OutputsAnnotations represents the metadata for chosen outputs. The key is the index of the output. -type OutputsAnnotations map[int]*OutputAnnotation - // NewDataOutputAnnotation constructs a new OutputAnnotation for the data output. func NewDataOutputAnnotation() *OutputAnnotation { return &OutputAnnotation{ diff --git a/engine/transaction/errors/errors.go b/engine/transaction/errors/errors.go index da8666033..bff9a158f 100644 --- a/engine/transaction/errors/errors.go +++ b/engine/transaction/errors/errors.go @@ -6,8 +6,8 @@ var ( // ErrTxOutlineSpecificationRequired is returned when a transaction outline is created with no specification. ErrTxOutlineSpecificationRequired = models.SPVError{Code: "tx-spec-spec-required", Message: "transaction outline requires a specification", StatusCode: 400} - // ErrTxOutlineSpecificationXPubIDRequired is returned when a transaction outline is created without xPubID. - ErrTxOutlineSpecificationXPubIDRequired = models.SPVError{Code: "tx-spec-spec-xpub-id-required", Message: "cannot create transaction outline without knowledge about xPubID", StatusCode: 500} + // ErrTxOutlineSpecificationUserIDRequired is returned when a transaction outline is created without UserID. + ErrTxOutlineSpecificationUserIDRequired = models.SPVError{Code: "tx-spec-spec-user-id-required", Message: "cannot create transaction outline without knowledge about userID", StatusCode: 500} // ErrTxOutlineRequiresAtLeastOneOutput is returned when a transaction outline is created with no outputs. ErrTxOutlineRequiresAtLeastOneOutput = models.SPVError{Code: "tx-spec-output-required", Message: "transaction outline requires at least one output", StatusCode: 400} @@ -24,6 +24,9 @@ var ( // ErrTxOutlineSenderPaymailAddressNoDefault is when it is not possible to determine the default address for the sender. ErrTxOutlineSenderPaymailAddressNoDefault = models.SPVError{Code: "error-tx-spec-paymail-address-no-default", Message: "cannot choose paymail address of the sender", StatusCode: 400} + // ErrTxOutlineInsufficientFunds is returned when user has not enough BSV in UTXOs to fulfil the transaction. + ErrTxOutlineInsufficientFunds = models.SPVError{Code: "tx-outline-not-enough-funds", Message: "not enough funds to make the transaction", StatusCode: 422} + // ErrFailedToDecodeHex is returned when hex decoding fails. ErrFailedToDecodeHex = models.SPVError{Code: "failed-to-decode-hex", Message: "failed to decode hex", StatusCode: 400} diff --git a/engine/transaction/outlines/create_op_return_draft_test.go b/engine/transaction/outlines/create_op_return_outline_test.go similarity index 85% rename from engine/transaction/outlines/create_op_return_draft_test.go rename to engine/transaction/outlines/create_op_return_outline_test.go index f1f56d847..2e00726aa 100644 --- a/engine/transaction/outlines/create_op_return_draft_test.go +++ b/engine/transaction/outlines/create_op_return_outline_test.go @@ -9,7 +9,6 @@ import ( "github.com/bitcoin-sv/spv-wallet/engine/tester/fixtures" txerrors "github.com/bitcoin-sv/spv-wallet/engine/transaction/errors" "github.com/bitcoin-sv/spv-wallet/engine/transaction/outlines" - "github.com/bitcoin-sv/spv-wallet/engine/transaction/outlines/outputs" "github.com/bitcoin-sv/spv-wallet/engine/transaction/outlines/testabilities" "github.com/bitcoin-sv/spv-wallet/models" "github.com/bitcoin-sv/spv-wallet/models/request/opreturn" @@ -21,32 +20,32 @@ func TestCreateOpReturnTransactionOutline(t *testing.T) { const maxOpPushDataSize = 0xFFFFFFFF successTests := map[string]struct { - opReturn *outputs.OpReturn + opReturn *outlines.OpReturn lockingScript string }{ "return transaction outline for single string": { - opReturn: &outputs.OpReturn{ + opReturn: &outlines.OpReturn{ DataType: opreturn.DataTypeStrings, Data: []string{"Example data"}, }, lockingScript: "006a0c4578616d706c652064617461", }, "return transaction outline for multiple strings": { - opReturn: &outputs.OpReturn{ + opReturn: &outlines.OpReturn{ DataType: opreturn.DataTypeStrings, Data: []string{"Example", " ", "data"}, }, lockingScript: "006a074578616d706c6501200464617461", }, "return transaction outline for single hex": { - opReturn: &outputs.OpReturn{ + opReturn: &outlines.OpReturn{ DataType: opreturn.DataTypeHexes, Data: []string{toHex("Example data")}, }, lockingScript: "006a0c4578616d706c652064617461", }, "return transaction outline for multiple hexes": { - opReturn: &outputs.OpReturn{ + opReturn: &outlines.OpReturn{ DataType: opreturn.DataTypeHexes, Data: []string{toHex("Example"), toHex(" "), toHex("data")}, }, @@ -62,8 +61,8 @@ func TestCreateOpReturnTransactionOutline(t *testing.T) { // and: spec := &outlines.TransactionSpec{ - XPubID: fixtures.Sender.XPubID(), - Outputs: outputs.NewSpecifications(test.opReturn), + UserID: fixtures.Sender.ID(), + Outputs: outlines.NewOutputsSpec(test.opReturn), } // when: @@ -82,35 +81,35 @@ func TestCreateOpReturnTransactionOutline(t *testing.T) { } errorTests := map[string]struct { - spec *outputs.OpReturn + spec *outlines.OpReturn expectedError models.SPVError }{ "return error for no data in default type": { - spec: &outputs.OpReturn{}, + spec: &outlines.OpReturn{}, expectedError: txerrors.ErrTxOutlineOpReturnDataRequired, }, "return error for no data string type": { - spec: &outputs.OpReturn{ + spec: &outlines.OpReturn{ DataType: opreturn.DataTypeStrings, }, expectedError: txerrors.ErrTxOutlineOpReturnDataRequired, }, "return error for invalid hex": { - spec: &outputs.OpReturn{ + spec: &outlines.OpReturn{ DataType: opreturn.DataTypeHexes, Data: []string{"invalid hex"}, }, expectedError: txerrors.ErrFailedToDecodeHex, }, "return error for unknown data type": { - spec: &outputs.OpReturn{ + spec: &outlines.OpReturn{ DataType: 123, Data: []string{"Example", " ", "data"}, }, expectedError: txerrors.ErrTxOutlineOpReturnUnsupportedDataType, }, "return error for to big string": { - spec: &outputs.OpReturn{ + spec: &outlines.OpReturn{ DataType: opreturn.DataTypeStrings, Data: []string{strings.Repeat("1", maxOpPushDataSize+1)}, }, @@ -126,8 +125,8 @@ func TestCreateOpReturnTransactionOutline(t *testing.T) { // and: spec := &outlines.TransactionSpec{ - XPubID: fixtures.Sender.XPubID(), - Outputs: outputs.NewSpecifications(test.spec), + UserID: fixtures.Sender.ID(), + Outputs: outlines.NewOutputsSpec(test.spec), } // when: diff --git a/engine/transaction/outlines/create_draft_test.go b/engine/transaction/outlines/create_outline_test.go similarity index 82% rename from engine/transaction/outlines/create_draft_test.go rename to engine/transaction/outlines/create_outline_test.go index c9a8f327e..4d0a2e982 100644 --- a/engine/transaction/outlines/create_draft_test.go +++ b/engine/transaction/outlines/create_outline_test.go @@ -7,7 +7,6 @@ import ( "github.com/bitcoin-sv/spv-wallet/engine/tester/fixtures" txerrors "github.com/bitcoin-sv/spv-wallet/engine/transaction/errors" "github.com/bitcoin-sv/spv-wallet/engine/transaction/outlines" - "github.com/bitcoin-sv/spv-wallet/engine/transaction/outlines/outputs" "github.com/bitcoin-sv/spv-wallet/engine/transaction/outlines/testabilities" "github.com/bitcoin-sv/spv-wallet/models" ) @@ -23,16 +22,16 @@ func TestCreateTransactionOutlineError(t *testing.T) { }, "return error for transaction spec without xPub Id": { spec: &outlines.TransactionSpec{}, - expectedError: txerrors.ErrTxOutlineSpecificationXPubIDRequired, + expectedError: txerrors.ErrTxOutlineSpecificationUserIDRequired, }, "return error for no outputs in transaction spec": { - spec: &outlines.TransactionSpec{XPubID: fixtures.Sender.XPubID()}, + spec: &outlines.TransactionSpec{UserID: fixtures.Sender.ID()}, expectedError: txerrors.ErrTxOutlineRequiresAtLeastOneOutput, }, "return error for empty output list in transaction spec": { spec: &outlines.TransactionSpec{ - XPubID: fixtures.Sender.XPubID(), - Outputs: outputs.NewSpecifications(), + UserID: fixtures.Sender.ID(), + Outputs: outlines.NewOutputsSpec(), }, expectedError: txerrors.ErrTxOutlineRequiresAtLeastOneOutput, }, diff --git a/engine/transaction/outlines/create_paymail_draft_test.go b/engine/transaction/outlines/create_paymail_outline_test.go similarity index 91% rename from engine/transaction/outlines/create_paymail_draft_test.go rename to engine/transaction/outlines/create_paymail_outline_test.go index e9e68a6c4..404b5a617 100644 --- a/engine/transaction/outlines/create_paymail_draft_test.go +++ b/engine/transaction/outlines/create_paymail_outline_test.go @@ -9,7 +9,6 @@ import ( "github.com/bitcoin-sv/spv-wallet/engine/tester/fixtures" txerrors "github.com/bitcoin-sv/spv-wallet/engine/transaction/errors" "github.com/bitcoin-sv/spv-wallet/engine/transaction/outlines" - "github.com/bitcoin-sv/spv-wallet/engine/transaction/outlines/outputs" "github.com/bitcoin-sv/spv-wallet/engine/transaction/outlines/testabilities" "github.com/bitcoin-sv/spv-wallet/models" "github.com/bitcoin-sv/spv-wallet/models/bsv" @@ -33,8 +32,8 @@ func TestCreatePaymailTransactionOutline(t *testing.T) { // and: spec := &outlines.TransactionSpec{ - XPubID: fixtures.Sender.XPubID(), - Outputs: outputs.NewSpecifications(&outputs.Paymail{ + UserID: fixtures.Sender.ID(), + Outputs: outlines.NewOutputsSpec(&outlines.Paymail{ To: recipient, Satoshis: transactionSatoshiValue, From: optional.Of(sender), @@ -75,8 +74,8 @@ func TestCreatePaymailTransactionOutline(t *testing.T) { // and: spec := &outlines.TransactionSpec{ - XPubID: fixtures.Sender.XPubID(), - Outputs: outputs.NewSpecifications(&outputs.Paymail{ + UserID: fixtures.Sender.ID(), + Outputs: outlines.NewOutputsSpec(&outlines.Paymail{ To: recipient, Satoshis: paymentSatoshiValue, From: optional.Of(sender), @@ -121,8 +120,8 @@ func TestCreatePaymailTransactionOutline(t *testing.T) { // and: spec := &outlines.TransactionSpec{ - XPubID: fixtures.UserWithMorePaymails.XPubID(), - Outputs: outputs.NewSpecifications(&outputs.Paymail{ + UserID: fixtures.UserWithMorePaymails.ID(), + Outputs: outlines.NewOutputsSpec(&outlines.Paymail{ To: recipient, Satoshis: transactionSatoshiValue, }), @@ -140,19 +139,19 @@ func TestCreatePaymailTransactionOutline(t *testing.T) { errorTests := map[string]struct { user fixtures.User - spec *outputs.Paymail + spec *outlines.Paymail expectedError models.SPVError }{ "return error for no paymail address": { user: fixtures.Sender, - spec: &outputs.Paymail{ + spec: &outlines.Paymail{ Satoshis: transactionSatoshiValue, }, expectedError: txerrors.ErrReceiverPaymailAddressIsInvalid, }, "return error for only alias without domain": { user: fixtures.Sender, - spec: &outputs.Paymail{ + spec: &outlines.Paymail{ To: "test", Satoshis: transactionSatoshiValue, }, @@ -160,7 +159,7 @@ func TestCreatePaymailTransactionOutline(t *testing.T) { }, "return error for domain without alias": { user: fixtures.Sender, - spec: &outputs.Paymail{ + spec: &outlines.Paymail{ To: "@example.com", Satoshis: transactionSatoshiValue, }, @@ -168,7 +167,7 @@ func TestCreatePaymailTransactionOutline(t *testing.T) { }, "return error for paymail with invalid alias": { user: fixtures.Sender, - spec: &outputs.Paymail{ + spec: &outlines.Paymail{ To: "$$$@example.com", Satoshis: transactionSatoshiValue, }, @@ -176,7 +175,7 @@ func TestCreatePaymailTransactionOutline(t *testing.T) { }, "return error for paymail with invalid domain": { user: fixtures.Sender, - spec: &outputs.Paymail{ + spec: &outlines.Paymail{ To: "test@example.com.$$$", Satoshis: transactionSatoshiValue, }, @@ -184,7 +183,7 @@ func TestCreatePaymailTransactionOutline(t *testing.T) { }, "return error for zero satoshis value": { user: fixtures.Sender, - spec: &outputs.Paymail{ + spec: &outlines.Paymail{ To: recipient, Satoshis: 0, }, @@ -192,14 +191,14 @@ func TestCreatePaymailTransactionOutline(t *testing.T) { }, "return error for no satoshis value": { user: fixtures.Sender, - spec: &outputs.Paymail{ + spec: &outlines.Paymail{ To: recipient, }, expectedError: txerrors.ErrOutputValueTooLow, }, "return error for sender paymail without domain": { user: fixtures.Sender, - spec: &outputs.Paymail{ + spec: &outlines.Paymail{ To: recipient, Satoshis: transactionSatoshiValue, From: optional.Of("sender"), @@ -208,7 +207,7 @@ func TestCreatePaymailTransactionOutline(t *testing.T) { }, "return error for sender paymail without alias": { user: fixtures.Sender, - spec: &outputs.Paymail{ + spec: &outlines.Paymail{ To: recipient, Satoshis: transactionSatoshiValue, From: optional.Of("@example.com"), @@ -217,7 +216,7 @@ func TestCreatePaymailTransactionOutline(t *testing.T) { }, "return error for sender paymail with invalid alias": { user: fixtures.Sender, - spec: &outputs.Paymail{ + spec: &outlines.Paymail{ To: recipient, Satoshis: transactionSatoshiValue, From: optional.Of("$$$@example.com"), @@ -226,7 +225,7 @@ func TestCreatePaymailTransactionOutline(t *testing.T) { }, "return error for sender paymail with invalid domain domain": { user: fixtures.Sender, - spec: &outputs.Paymail{ + spec: &outlines.Paymail{ To: recipient, Satoshis: transactionSatoshiValue, From: optional.Of("sender@example.com.$$$"), @@ -235,7 +234,7 @@ func TestCreatePaymailTransactionOutline(t *testing.T) { }, "return error for sender paymail address not existing in our system": { user: fixtures.Sender, - spec: &outputs.Paymail{ + spec: &outlines.Paymail{ To: recipient, Satoshis: transactionSatoshiValue, From: optional.Of(fixtures.RecipientExternal.DefaultPaymail()), @@ -244,7 +243,7 @@ func TestCreatePaymailTransactionOutline(t *testing.T) { }, "return error for sender paymail not belonging to that user": { user: fixtures.Sender, - spec: &outputs.Paymail{ + spec: &outlines.Paymail{ To: recipient, Satoshis: transactionSatoshiValue, From: optional.Of(fixtures.RecipientInternal.DefaultPaymail()), @@ -253,7 +252,7 @@ func TestCreatePaymailTransactionOutline(t *testing.T) { }, "return error for default sender paymail of user without paymail": { user: fixtures.UserWithoutPaymail, - spec: &outputs.Paymail{ + spec: &outlines.Paymail{ To: recipient, Satoshis: transactionSatoshiValue, }, @@ -272,8 +271,8 @@ func TestCreatePaymailTransactionOutline(t *testing.T) { // and: spec := &outlines.TransactionSpec{ - XPubID: test.user.XPubID(), - Outputs: outputs.NewSpecifications(test.spec), + UserID: test.user.ID(), + Outputs: outlines.NewOutputsSpec(test.spec), } // when: @@ -337,8 +336,8 @@ func TestCreatePaymailTransactionOutline(t *testing.T) { // and: spec := &outlines.TransactionSpec{ - XPubID: fixtures.Sender.XPubID(), - Outputs: outputs.NewSpecifications(&outputs.Paymail{ + UserID: fixtures.Sender.ID(), + Outputs: outlines.NewOutputsSpec(&outlines.Paymail{ To: recipient, Satoshis: transactionSatoshiValue, }), diff --git a/engine/transaction/outlines/internal/evaluation/context.go b/engine/transaction/outlines/evaluation_context.go similarity index 53% rename from engine/transaction/outlines/internal/evaluation/context.go rename to engine/transaction/outlines/evaluation_context.go index b7b43a0a1..324f53c00 100644 --- a/engine/transaction/outlines/internal/evaluation/context.go +++ b/engine/transaction/outlines/evaluation_context.go @@ -1,4 +1,4 @@ -package evaluation +package outlines import ( "context" @@ -8,19 +8,29 @@ import ( "github.com/rs/zerolog" ) +// evaluationContext is a context for the evaluation of a transaction outline specification. +type evaluationContext interface { + context.Context + XPubID() string + UserID() string + Log() *zerolog.Logger + Paymail() paymail.ServiceClient + PaymailAddressService() paymailaddress.Service +} + type ctx struct { context.Context - xPubID string + userID string log *zerolog.Logger paymail paymail.ServiceClient paymailAddressService paymailaddress.Service } -// NewContext creates a new context -func NewContext(c context.Context, xPubID string, log *zerolog.Logger, paymail paymail.ServiceClient, paymailAddressService paymailaddress.Service) Context { +// newTransactionContext creates a new context +func newTransactionContext(c context.Context, userID string, log *zerolog.Logger, paymail paymail.ServiceClient, paymailAddressService paymailaddress.Service) evaluationContext { return &ctx{ Context: c, - xPubID: xPubID, + userID: userID, log: log, paymail: paymail, paymailAddressService: paymailAddressService, @@ -28,7 +38,11 @@ func NewContext(c context.Context, xPubID string, log *zerolog.Logger, paymail p } func (c *ctx) XPubID() string { - return c.xPubID + return "" +} + +func (c *ctx) UserID() string { + return c.userID } func (c *ctx) Log() *zerolog.Logger { diff --git a/engine/transaction/outlines/inteface.go b/engine/transaction/outlines/inteface.go deleted file mode 100644 index cb747849b..000000000 --- a/engine/transaction/outlines/inteface.go +++ /dev/null @@ -1,8 +0,0 @@ -package outlines - -import "context" - -// Service is a service for creating transaction outlines. -type Service interface { - Create(ctx context.Context, spec *TransactionSpec) (*Transaction, error) -} diff --git a/engine/transaction/outlines/interface.go b/engine/transaction/outlines/interface.go new file mode 100644 index 000000000..2accf2a8f --- /dev/null +++ b/engine/transaction/outlines/interface.go @@ -0,0 +1,24 @@ +package outlines + +import ( + "context" + + "github.com/bitcoin-sv/spv-wallet/engine/transaction" +) + +// PaymailAddressService is a component that provides methods for working with paymail address. +type PaymailAddressService interface { + HasPaymailAddress(ctx context.Context, userID string, address string) (bool, error) + GetDefaultPaymailAddress(ctx context.Context, userID string) (string, error) +} + +// Service is a service for creating transaction outlines. +type Service interface { + Create(ctx context.Context, spec *TransactionSpec) (*Transaction, error) +} + +// Transaction represents a transaction outline. +type Transaction struct { + BEEF string + Annotations transaction.Annotations +} diff --git a/engine/transaction/outlines/internal/evaluation/interface.go b/engine/transaction/outlines/internal/evaluation/interface.go deleted file mode 100644 index c6b406c2d..000000000 --- a/engine/transaction/outlines/internal/evaluation/interface.go +++ /dev/null @@ -1,18 +0,0 @@ -package evaluation - -import ( - "context" - - "github.com/bitcoin-sv/spv-wallet/engine/paymail" - "github.com/bitcoin-sv/spv-wallet/engine/paymailaddress" - "github.com/rs/zerolog" -) - -// Context is a context for the evaluation of a transaction outline specification. -type Context interface { - context.Context - XPubID() string - Log() *zerolog.Logger - Paymail() paymail.ServiceClient - PaymailAddressService() paymailaddress.Service -} diff --git a/engine/transaction/outlines/outputs/op_return.go b/engine/transaction/outlines/output_op_return.go similarity index 90% rename from engine/transaction/outlines/outputs/op_return.go rename to engine/transaction/outlines/output_op_return.go index 89cd22f4e..a92b3a88c 100644 --- a/engine/transaction/outlines/outputs/op_return.go +++ b/engine/transaction/outlines/output_op_return.go @@ -1,4 +1,4 @@ -package outputs +package outlines import ( "encoding/hex" @@ -9,14 +9,13 @@ import ( "github.com/bitcoin-sv/spv-wallet/engine/spverrors" "github.com/bitcoin-sv/spv-wallet/engine/transaction" txerrors "github.com/bitcoin-sv/spv-wallet/engine/transaction/errors" - "github.com/bitcoin-sv/spv-wallet/engine/transaction/outlines/internal/evaluation" "github.com/bitcoin-sv/spv-wallet/models/request/opreturn" ) // OpReturn represents an OP_RETURN output specification. type OpReturn opreturn.Output -func (o *OpReturn) evaluate(evaluation.Context) (annotatedOutputs, error) { +func (o *OpReturn) evaluate(evaluationContext) (annotatedOutputs, error) { if len(o.Data) == 0 { return nil, txerrors.ErrTxOutlineOpReturnDataRequired } diff --git a/engine/transaction/outlines/outputs/paymail.go b/engine/transaction/outlines/output_paymail.go similarity index 85% rename from engine/transaction/outlines/outputs/paymail.go rename to engine/transaction/outlines/output_paymail.go index 1dbd093c5..1db7da07f 100644 --- a/engine/transaction/outlines/outputs/paymail.go +++ b/engine/transaction/outlines/output_paymail.go @@ -1,4 +1,4 @@ -package outputs +package outlines import ( "errors" @@ -10,7 +10,6 @@ import ( "github.com/bitcoin-sv/spv-wallet/engine/spverrors" "github.com/bitcoin-sv/spv-wallet/engine/transaction" txerrors "github.com/bitcoin-sv/spv-wallet/engine/transaction/errors" - "github.com/bitcoin-sv/spv-wallet/engine/transaction/outlines/internal/evaluation" paymailreq "github.com/bitcoin-sv/spv-wallet/models/request/paymail" "github.com/bitcoin-sv/spv-wallet/models/transaction/bucket" ) @@ -18,7 +17,7 @@ import ( // Paymail represents a paymail output type Paymail paymailreq.Output -func (p *Paymail) evaluate(ctx evaluation.Context) (annotatedOutputs, error) { +func (p *Paymail) evaluate(ctx evaluationContext) (annotatedOutputs, error) { paymailClient := ctx.Paymail() receiverAddress, err := paymailClient.GetSanitizedPaymail(p.To) @@ -73,7 +72,7 @@ func (p *Paymail) createBsvPaymailOutput(output *paymail.PaymentOutput, referenc }, nil } -func (p *Paymail) sender(ctx evaluation.Context) (string, error) { +func (p *Paymail) sender(ctx evaluationContext) (string, error) { if p.From == nil { return p.defaultSenderAddress(ctx) } @@ -86,18 +85,18 @@ func (p *Paymail) sender(ctx evaluation.Context) (string, error) { return *p.From, nil } -func (p *Paymail) validateProvidedSenderPaymail(ctx evaluation.Context) error { +func (p *Paymail) validateProvidedSenderPaymail(ctx evaluationContext) error { var sender = *p.From _, err := ctx.Paymail().GetSanitizedPaymail(sender) if err != nil { return txerrors.ErrSenderPaymailAddressIsInvalid.Wrap(err) } - ownsPaymail, err := ctx.PaymailAddressService().HasPaymailAddress(ctx, ctx.XPubID(), sender) + ownsPaymail, err := ctx.PaymailAddressService().HasPaymailAddress(ctx, ctx.UserID(), sender) if errors.Is(err, spverrors.ErrCouldNotFindPaymail) { return txerrors.ErrSenderPaymailAddressIsInvalid.Wrap(err) } if err != nil { - return spverrors.Wrapf(err, "failed to check if paymail %s belongs to xpub %s", sender, ctx.XPubID()) + return spverrors.Wrapf(err, "failed to check if paymail %s belongs to user %s", sender, ctx.UserID()) } if !ownsPaymail { @@ -107,8 +106,8 @@ func (p *Paymail) validateProvidedSenderPaymail(ctx evaluation.Context) error { return nil } -func (p *Paymail) defaultSenderAddress(ctx evaluation.Context) (string, error) { - sender, err := ctx.PaymailAddressService().GetDefaultPaymailAddress(ctx, ctx.XPubID()) +func (p *Paymail) defaultSenderAddress(ctx evaluationContext) (string, error) { + sender, err := ctx.PaymailAddressService().GetDefaultPaymailAddress(ctx, ctx.UserID()) if err != nil { return "", txerrors.ErrTxOutlineSenderPaymailAddressNoDefault.Wrap(err) } diff --git a/engine/transaction/outlines/outputs/spec.go b/engine/transaction/outlines/output_specifications.go similarity index 69% rename from engine/transaction/outlines/outputs/spec.go rename to engine/transaction/outlines/output_specifications.go index afc73cf80..c6a598c5e 100644 --- a/engine/transaction/outlines/outputs/spec.go +++ b/engine/transaction/outlines/output_specifications.go @@ -1,37 +1,36 @@ -package outputs +package outlines import ( sdk "github.com/bitcoin-sv/go-sdk/transaction" "github.com/bitcoin-sv/spv-wallet/engine/spverrors" "github.com/bitcoin-sv/spv-wallet/engine/transaction" txerrors "github.com/bitcoin-sv/spv-wallet/engine/transaction/errors" - "github.com/bitcoin-sv/spv-wallet/engine/transaction/outlines/internal/evaluation" ) -// Specifications are representing a client specification for outputs part of the transaction. -type Specifications struct { - Outputs []Spec +// OutputsSpec are representing a client specification for outputs part of the transaction. +type OutputsSpec struct { + Outputs []OutputSpec } -// Spec is a specification for a single output of the transaction. -type Spec interface { - evaluate(ctx evaluation.Context) (annotatedOutputs, error) +// OutputSpec is a specification for a single output of the transaction. +type OutputSpec interface { + evaluate(ctx evaluationContext) (annotatedOutputs, error) } -// NewSpecifications constructs a new Specifications instance with provided outputs specifications. -func NewSpecifications(outputs ...Spec) *Specifications { - return &Specifications{ +// NewOutputsSpec constructs a new OutputsSpec instance with provided outputs specifications. +func NewOutputsSpec(outputs ...OutputSpec) OutputsSpec { + return OutputsSpec{ Outputs: outputs, } } // Add a new output specification to the list of outputs. -func (s *Specifications) Add(output Spec) { +func (s *OutputsSpec) Add(output OutputSpec) { s.Outputs = append(s.Outputs, output) } // Evaluate the outputs specifications and return the transaction outputs and their annotations. -func (s *Specifications) Evaluate(ctx evaluation.Context) ([]*sdk.TransactionOutput, transaction.OutputsAnnotations, error) { +func (s *OutputsSpec) Evaluate(ctx evaluationContext) ([]*sdk.TransactionOutput, transaction.OutputsAnnotations, error) { if s.Outputs == nil { return nil, nil, txerrors.ErrTxOutlineRequiresAtLeastOneOutput } @@ -44,7 +43,11 @@ func (s *Specifications) Evaluate(ctx evaluation.Context) ([]*sdk.TransactionOut return txOutputs, annotations, nil } -func (s *Specifications) evaluate(ctx evaluation.Context) (annotatedOutputs, error) { +func (s *OutputsSpec) evaluate(ctx evaluationContext) (annotatedOutputs, error) { + if len(s.Outputs) == 0 { + return nil, txerrors.ErrTxOutlineRequiresAtLeastOneOutput + } + outputs := make(annotatedOutputs, 0) for _, spec := range s.Outputs { outs, err := spec.evaluate(ctx) @@ -56,13 +59,13 @@ func (s *Specifications) evaluate(ctx evaluation.Context) (annotatedOutputs, err return outputs, nil } +type annotatedOutputs []*annotatedOutput + type annotatedOutput struct { *transaction.OutputAnnotation *sdk.TransactionOutput } -type annotatedOutputs []*annotatedOutput - func singleAnnotatedOutput(txOut *sdk.TransactionOutput, out *transaction.OutputAnnotation) annotatedOutputs { return annotatedOutputs{ &annotatedOutput{ diff --git a/engine/transaction/outlines/testabilities/ability_draft_transaction.go b/engine/transaction/outlines/testabilities/ability_outline_transaction.go similarity index 100% rename from engine/transaction/outlines/testabilities/ability_draft_transaction.go rename to engine/transaction/outlines/testabilities/ability_outline_transaction.go diff --git a/engine/transaction/outlines/testabilities/assert_outline_input.go b/engine/transaction/outlines/testabilities/assert_outline_input.go new file mode 100644 index 000000000..2fce257d7 --- /dev/null +++ b/engine/transaction/outlines/testabilities/assert_outline_input.go @@ -0,0 +1,31 @@ +package testabilities + +import ( + "testing" + + sdk "github.com/bitcoin-sv/go-sdk/transaction" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type txInputAssertion struct { + t testing.TB + parent *assertion + assert *assert.Assertions + require *require.Assertions + input *sdk.TransactionInput + annotation any + index int +} + +func (a *txInputAssertion) HasSourceTxID(id string) InputAssertion { + a.t.Helper() + a.assert.Equal(id, a.input.SourceTXID) + return a +} + +func (a *txInputAssertion) HasSourceVout(index int) InputAssertion { + a.t.Helper() + a.assert.Equal(index, a.input.SourceTxOutIndex) + return a +} diff --git a/engine/transaction/outlines/testabilities/assert_outline_output.go b/engine/transaction/outlines/testabilities/assert_outline_output.go new file mode 100644 index 000000000..40738b72f --- /dev/null +++ b/engine/transaction/outlines/testabilities/assert_outline_output.go @@ -0,0 +1,79 @@ +package testabilities + +import ( + "testing" + + sdk "github.com/bitcoin-sv/go-sdk/transaction" + "github.com/bitcoin-sv/spv-wallet/engine/transaction" + "github.com/bitcoin-sv/spv-wallet/models/bsv" + "github.com/bitcoin-sv/spv-wallet/models/transaction/bucket" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type txOutputAssertion struct { + t testing.TB + parent *assertion + assert *assert.Assertions + require *require.Assertions + txout *sdk.TransactionOutput + annotation *transaction.OutputAnnotation + index int +} + +func (a *txOutputAssertion) HasBucket(bucket bucket.Name) OutputAssertion { + a.t.Helper() + a.require.NotNil(a.annotation, "Output %d has no annotation", a.index) + a.assert.Equal(bucket, a.annotation.Bucket, "Output %d has invalid bucket annotation", a.index) + return a +} + +func (a *txOutputAssertion) HasSatoshis(satoshis bsv.Satoshis) OutputAssertion { + a.t.Helper() + a.assert.EqualValues(satoshis, a.txout.Satoshis, "Output %d has invalid satoshis value", a.index) + return a +} + +func (a *txOutputAssertion) HasLockingScript(lockingScript string) OutputAssertion { + a.t.Helper() + a.assert.Equal(lockingScript, a.txout.LockingScriptHex(), "Output %d has invalid locking script", a.index) + return a +} + +func (a *txOutputAssertion) IsDataOnly() OutputAssertion { + a.t.Helper() + a.assert.Zerof(a.txout.Satoshis, "Output %d has value in satoshis which is not allowed for data only outputs", a.index) + a.assert.True(a.txout.LockingScript.IsData(), "Output %d has locking script which is not data script", a.index) + return a +} + +func (a *txOutputAssertion) IsPaymail() TransactionOutlinePaymailOutputAssertion { + a.t.Helper() + a.require.NotNil(a.annotation, "Output %d has no annotation", a.index) + a.assert.NotNil(a.annotation.Paymail, "Output %d is not a paymail output", a.index) + return a +} + +func (a *txOutputAssertion) HasReceiver(receiver string) TransactionOutlinePaymailOutputAssertion { + a.t.Helper() + if a.annotation.Paymail != nil { + a.assert.Equal(receiver, a.annotation.Paymail.Receiver, "Output %d has invalid paymail receiver", a.index) + } + return a +} + +func (a *txOutputAssertion) HasSender(sender string) TransactionOutlinePaymailOutputAssertion { + a.t.Helper() + if a.annotation.Paymail != nil { + a.assert.Equal(sender, a.annotation.Paymail.Sender, "Output %d has invalid paymail sender", a.index) + } + return a +} + +func (a *txOutputAssertion) HasReference(reference string) TransactionOutlinePaymailOutputAssertion { + a.t.Helper() + if a.annotation.Paymail != nil { + a.assert.Equal(reference, a.annotation.Paymail.Reference, "Output %d has invalid paymail reference", a.index) + } + return a +} diff --git a/engine/transaction/outlines/testabilities/assert_draft_transaction.go b/engine/transaction/outlines/testabilities/assert_outline_transaction.go similarity index 51% rename from engine/transaction/outlines/testabilities/assert_draft_transaction.go rename to engine/transaction/outlines/testabilities/assert_outline_transaction.go index 92ba6055f..33e40dd78 100644 --- a/engine/transaction/outlines/testabilities/assert_draft_transaction.go +++ b/engine/transaction/outlines/testabilities/assert_outline_transaction.go @@ -4,7 +4,6 @@ import ( "testing" sdk "github.com/bitcoin-sv/go-sdk/transaction" - "github.com/bitcoin-sv/spv-wallet/engine/transaction" "github.com/bitcoin-sv/spv-wallet/engine/transaction/outlines" "github.com/bitcoin-sv/spv-wallet/models/bsv" "github.com/bitcoin-sv/spv-wallet/models/transaction/bucket" @@ -30,11 +29,17 @@ type SuccessfullyCreatedTransactionOutlineAssertion interface { } type WithParseableBEEFTransactionOutlineAssertion interface { + HasInputs(count int) WithParseableBEEFTransactionOutlineAssertion + Input(index int) InputAssertion HasOutputs(count int) WithParseableBEEFTransactionOutlineAssertion - HasOutput(index int, assert func(OutputAssertion)) WithParseableBEEFTransactionOutlineAssertion Output(index int) OutputAssertion } +type InputAssertion interface { + HasSourceTxID(id string) InputAssertion + HasSourceVout(index int) InputAssertion +} + type OutputAssertion interface { HasBucket(bucket bucket.Name) OutputAssertion HasSatoshis(satoshis bsv.Satoshis) OutputAssertion @@ -63,11 +68,13 @@ type assertion struct { } func (a *assertion) Created(transaction *outlines.Transaction) CreatedTransactionOutlineAssertion { + a.t.Helper() a.txOutline = transaction return a } func (a *assertion) WithError(err error) ErrorCreationTransactionOutlineAssertion { + a.t.Helper() a.assert.Nil(a.txOutline) a.assert.Error(err) a.err = err @@ -75,13 +82,15 @@ func (a *assertion) WithError(err error) ErrorCreationTransactionOutlineAssertio } func (a *assertion) ThatIs(expectedError error) { + a.t.Helper() 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. func (a *assertion) WithNoError(err error) SuccessfullyCreatedTransactionOutlineAssertion { + a.t.Helper() a.require.NoError(err, "Creation of transaction outline has finished with error") - a.require.NotNil(a.txOutline, "Transaction outline should be created if there is no error") + a.require.NotNil(a.txOutline, "The result is nil although there was no error") return a } @@ -91,36 +100,45 @@ func (a *assertion) WithParseableBEEFHex() WithParseableBEEFTransactionOutlineAs var err error a.tx, err = sdk.NewTransactionFromBEEFHex(a.txOutline.BEEF) - a.require.NoErrorf(err, "Transaction outline has invalid BEEF hex: %s", a.txOutline.BEEF) + a.require.NoErrorf(err, "Invalid BEEF hex: %s", a.txOutline.BEEF) return a } -func (a *assertion) HasOutputs(count int) WithParseableBEEFTransactionOutlineAssertion { - a.require.Lenf(a.tx.Outputs, count, "BEEF of transaction outline has invalid number of outputs") - a.require.Lenf(a.txOutline.Annotations.Outputs, count, "Annotations of transaction outline has invalid number of outputs") +func (a *assertion) HasInputs(count int) WithParseableBEEFTransactionOutlineAssertion { + a.t.Helper() + a.require.Lenf(a.tx.Inputs, count, "Number of Transaction Inputs") return a } -type txOutputAssertion struct { - parent *assertion - assert *assert.Assertions - require *require.Assertions - txout *sdk.TransactionOutput - annotation *transaction.OutputAnnotation - index int +func (a *assertion) Input(index int) InputAssertion { + a.t.Helper() + a.require.Greater(len(a.tx.Inputs), index, "Transaction Inputs doesn't have input %d", index) + + return &txInputAssertion{ + parent: a, + t: a.t, + assert: a.assert, + require: a.require, + input: a.tx.Inputs[index], + annotation: nil, + index: index, + } } -func (a *assertion) HasOutput(index int, assert func(OutputAssertion)) WithParseableBEEFTransactionOutlineAssertion { - assert(a.Output(index)) +func (a *assertion) HasOutputs(count int) WithParseableBEEFTransactionOutlineAssertion { + a.t.Helper() + a.require.Lenf(a.tx.Outputs, count, "Number of Transaction Outputs") + a.require.Lenf(a.txOutline.Annotations.Outputs, count, "Number of Output Annotations") return a } func (a *assertion) Output(index int) OutputAssertion { - a.require.Greater(len(a.tx.Outputs), index, "Transaction outline outputs has no element at index %d", index) - a.require.Greater(len(a.txOutline.Annotations.Outputs), index, "Transaction outline annotation outputs has no element at index %d", index) + a.t.Helper() + a.require.Greater(len(a.tx.Outputs), index, "Transaction Outputs doesn't have output %d", index) return &txOutputAssertion{ parent: a, + t: a.t, assert: a.assert, require: a.require, txout: a.tx.Outputs[index], @@ -128,50 +146,3 @@ func (a *assertion) Output(index int) OutputAssertion { index: index, } } - -func (a *txOutputAssertion) HasBucket(bucket bucket.Name) OutputAssertion { - a.assert.Equal(bucket, a.annotation.Bucket, "Output %d has invalid bucket annotation", a.index) - return a -} - -func (a *txOutputAssertion) HasSatoshis(satoshis bsv.Satoshis) OutputAssertion { - a.assert.EqualValues(satoshis, a.txout.Satoshis, "Output %d has invalid satoshis value", a.index) - return a -} - -func (a *txOutputAssertion) HasLockingScript(lockingScript string) OutputAssertion { - a.assert.Equal(lockingScript, a.txout.LockingScriptHex(), "Output %d has invalid locking script", a.index) - return a -} - -func (a *txOutputAssertion) IsDataOnly() OutputAssertion { - a.assert.Zerof(a.txout.Satoshis, "Output %d has value in satoshis which is not allowed for data only outputs", a.index) - a.assert.True(a.txout.LockingScript.IsData(), "Output %d has locking script which is not data script", a.index) - return a -} - -func (a *txOutputAssertion) IsPaymail() TransactionOutlinePaymailOutputAssertion { - a.assert.NotNil(a.annotation.Paymail, "Output %d is not a paymail output", a.index) - return a -} - -func (a *txOutputAssertion) HasReceiver(receiver string) TransactionOutlinePaymailOutputAssertion { - if a.annotation.Paymail != nil { - a.assert.Equal(receiver, a.annotation.Paymail.Receiver, "Output %d has invalid paymail receiver", a.index) - } - return a -} - -func (a *txOutputAssertion) HasSender(sender string) TransactionOutlinePaymailOutputAssertion { - if a.annotation.Paymail != nil { - a.assert.Equal(sender, a.annotation.Paymail.Sender, "Output %d has invalid paymail sender", a.index) - } - return a -} - -func (a *txOutputAssertion) HasReference(reference string) TransactionOutlinePaymailOutputAssertion { - if a.annotation.Paymail != nil { - a.assert.Equal(reference, a.annotation.Paymail.Reference, "Output %d has invalid paymail reference", a.index) - } - return a -} diff --git a/engine/transaction/outlines/testabilities/fixture_draft_transaction.go b/engine/transaction/outlines/testabilities/fixture_outline_transaction.go similarity index 77% rename from engine/transaction/outlines/testabilities/fixture_draft_transaction.go rename to engine/transaction/outlines/testabilities/fixture_outline_transaction.go index d21c83c41..2db84e035 100644 --- a/engine/transaction/outlines/testabilities/fixture_draft_transaction.go +++ b/engine/transaction/outlines/testabilities/fixture_outline_transaction.go @@ -4,7 +4,6 @@ import ( "testing" tpaymail "github.com/bitcoin-sv/spv-wallet/engine/paymail/testabilities" - tpaymailaddress "github.com/bitcoin-sv/spv-wallet/engine/paymailaddress/testabilities" "github.com/bitcoin-sv/spv-wallet/engine/tester" "github.com/bitcoin-sv/spv-wallet/engine/transaction/outlines" ) @@ -18,7 +17,7 @@ type TransactionOutlineFixture interface { type transactionOutlineAbility struct { t testing.TB paymailClientAbility tpaymail.PaymailClientFixture - paymailAddressAbility tpaymailaddress.PaymailAddressServiceFixture + paymailAddressService outlines.PaymailAddressService } // Given creates a new test fixture. @@ -26,7 +25,7 @@ func Given(t testing.TB) (given TransactionOutlineFixture) { ability := &transactionOutlineAbility{ t: t, paymailClientAbility: tpaymail.Given(t), - paymailAddressAbility: tpaymailaddress.Given(t), + paymailAddressService: newPaymailAddressServiceMock(t), } return ability } @@ -38,5 +37,9 @@ func (a *transactionOutlineAbility) ExternalRecipientHost() tpaymail.PaymailHost // NewTransactionOutlinesService creates a new transaction outline service to use in tests. func (a *transactionOutlineAbility) NewTransactionOutlinesService() outlines.Service { - return outlines.NewService(a.paymailClientAbility.NewPaymailClientService(), a.paymailAddressAbility.NewPaymailAddressService(), tester.Logger(a.t)) + return outlines.NewService( + a.paymailClientAbility.NewPaymailClientService(), + a.paymailAddressService, + tester.Logger(a.t), + ) } diff --git a/engine/transaction/outlines/testabilities/mock_paymail_address_service.go b/engine/transaction/outlines/testabilities/mock_paymail_address_service.go new file mode 100644 index 000000000..3163e4c4e --- /dev/null +++ b/engine/transaction/outlines/testabilities/mock_paymail_address_service.go @@ -0,0 +1,42 @@ +package testabilities + +import ( + "context" + "slices" + "testing" + + "github.com/bitcoin-sv/spv-wallet/engine/paymailaddress/paerrors" + "github.com/bitcoin-sv/spv-wallet/engine/tester/fixtures" +) + +type mockPaymailAddressService struct { + t testing.TB + users []fixtures.User +} + +func newPaymailAddressServiceMock(t testing.TB) *mockPaymailAddressService { + return &mockPaymailAddressService{ + t: t, + users: fixtures.InternalUsers(), + } +} + +func (m *mockPaymailAddressService) HasPaymailAddress(_ context.Context, userID string, address string) (bool, error) { + for _, user := range m.users { + if user.ID() == userID { + return slices.Contains(user.Paymails, address), nil + } + } + return false, nil +} + +func (m *mockPaymailAddressService) GetDefaultPaymailAddress(_ context.Context, userID string) (string, error) { + for _, user := range m.users { + if user.ID() == userID { + if user.DefaultPaymail() != "" { + return user.DefaultPaymail(), nil + } + } + } + return "", paerrors.ErrNoDefaultPaymailAddress +} diff --git a/engine/transaction/outlines/transaction.go b/engine/transaction/outlines/transaction.go deleted file mode 100644 index 590bdc54e..000000000 --- a/engine/transaction/outlines/transaction.go +++ /dev/null @@ -1,9 +0,0 @@ -package outlines - -import "github.com/bitcoin-sv/spv-wallet/engine/transaction" - -// Transaction represents a transaction outline. -type Transaction struct { - BEEF string - Annotations transaction.Annotations -} diff --git a/engine/transaction/outlines/transaction_outlines_service.go b/engine/transaction/outlines/transaction_outlines_service.go index c80965767..d6a112260 100644 --- a/engine/transaction/outlines/transaction_outlines_service.go +++ b/engine/transaction/outlines/transaction_outlines_service.go @@ -3,13 +3,10 @@ package outlines import ( "context" - sdk "github.com/bitcoin-sv/go-sdk/transaction" "github.com/bitcoin-sv/spv-wallet/engine/paymail" "github.com/bitcoin-sv/spv-wallet/engine/paymailaddress" "github.com/bitcoin-sv/spv-wallet/engine/spverrors" - "github.com/bitcoin-sv/spv-wallet/engine/transaction" txerrors "github.com/bitcoin-sv/spv-wallet/engine/transaction/errors" - "github.com/bitcoin-sv/spv-wallet/engine/transaction/outlines/internal/evaluation" "github.com/rs/zerolog" ) @@ -26,7 +23,7 @@ func NewService(paymailService paymail.ServiceClient, paymailAddressService paym } if paymailAddressService == nil { - panic("paymailaddress.Service is required to create transaction outlines service") + panic("PaymailAddressService is required to create transaction outlines service") } return &service{ @@ -42,36 +39,30 @@ func (s *service) Create(ctx context.Context, spec *TransactionSpec) (*Transacti return nil, txerrors.ErrTxOutlineSpecificationRequired } - if spec.XPubID == "" { - return nil, txerrors.ErrTxOutlineSpecificationXPubIDRequired + if spec.UserID == "" { + return nil, txerrors.ErrTxOutlineSpecificationUserIDRequired } - c := evaluation.NewContext( + c := newTransactionContext( ctx, - spec.XPubID, + spec.UserID, s.logger, s.paymailService, s.paymailAddressService, ) - outputs, annotations, err := spec.outputs(c) + tx, annotations, err := spec.evaluate(c) if err != nil { return nil, err } - tx := &sdk.Transaction{ - Outputs: outputs, - } - beef, err := tx.BEEFHex() if err != nil { return nil, spverrors.Wrapf(err, "failed to create transaction outline") } return &Transaction{ - BEEF: beef, - Annotations: transaction.Annotations{ - Outputs: annotations, - }, + BEEF: beef, + Annotations: annotations, }, nil } diff --git a/engine/transaction/outlines/transaction_spec.go b/engine/transaction/outlines/transaction_spec.go index 2620780e4..a64bd9a4f 100644 --- a/engine/transaction/outlines/transaction_spec.go +++ b/engine/transaction/outlines/transaction_spec.go @@ -4,26 +4,28 @@ import ( sdk "github.com/bitcoin-sv/go-sdk/transaction" "github.com/bitcoin-sv/spv-wallet/engine/spverrors" "github.com/bitcoin-sv/spv-wallet/engine/transaction" - txerrors "github.com/bitcoin-sv/spv-wallet/engine/transaction/errors" - "github.com/bitcoin-sv/spv-wallet/engine/transaction/outlines/internal/evaluation" - "github.com/bitcoin-sv/spv-wallet/engine/transaction/outlines/outputs" ) // TransactionSpec represents client provided specification for a transaction outline. type TransactionSpec struct { - Outputs *outputs.Specifications - XPubID string + Outputs OutputsSpec + UserID string } -func (t *TransactionSpec) outputs(ctx evaluation.Context) ([]*sdk.TransactionOutput, transaction.OutputsAnnotations, error) { - if t.Outputs == nil { - return nil, nil, txerrors.ErrTxOutlineRequiresAtLeastOneOutput +func (t *TransactionSpec) evaluate(ctx evaluationContext) (*sdk.Transaction, transaction.Annotations, error) { + outputs, err := t.Outputs.evaluate(ctx) + if err != nil { + return nil, transaction.Annotations{}, spverrors.Wrapf(err, "failed to evaluate outputs") } - outs, annotations, err := t.Outputs.Evaluate(ctx) - if err != nil { - return nil, nil, spverrors.Wrapf(err, "failed to evaluate outputs") + txOuts, outputsAnnotations := outputs.splitIntoTransactionOutputsAndAnnotations() + tx := &sdk.Transaction{ + Outputs: txOuts, + } + + annotations := transaction.Annotations{ + Outputs: outputsAnnotations, } - return outs, annotations, nil + return tx, annotations, nil } diff --git a/engine/transaction/outlines/internal/inputs/inputs_query_composer.go b/engine/transaction/outlines/utxo/internal/sql/inputs_query_composer.go similarity index 99% rename from engine/transaction/outlines/internal/inputs/inputs_query_composer.go rename to engine/transaction/outlines/utxo/internal/sql/inputs_query_composer.go index b51d34ab9..6c6d2a59c 100644 --- a/engine/transaction/outlines/internal/inputs/inputs_query_composer.go +++ b/engine/transaction/outlines/utxo/internal/sql/inputs_query_composer.go @@ -1,4 +1,4 @@ -package inputs +package sql import ( "database/sql" diff --git a/engine/transaction/outlines/internal/inputs/selector.go b/engine/transaction/outlines/utxo/internal/sql/selector.go similarity index 91% rename from engine/transaction/outlines/internal/inputs/selector.go rename to engine/transaction/outlines/utxo/internal/sql/selector.go index 734c19649..2aeecae7b 100644 --- a/engine/transaction/outlines/internal/inputs/selector.go +++ b/engine/transaction/outlines/utxo/internal/sql/selector.go @@ -1,4 +1,4 @@ -package inputs +package sql import ( "context" @@ -14,8 +14,8 @@ import ( const txIdColumn = "tx_id" const voutColumn = "vout" -// Selector is a service that selects inputs for transaction. -type Selector interface { +// UTXOSelector is a service that selects inputs for transaction. +type UTXOSelector interface { SelectInputsForTransaction(ctx context.Context, userID string, satoshis bsv.Satoshis, byteSizeOfTxBeforeAddingSelectedInputs uint64) ([]*database.UserUTXO, error) } @@ -31,8 +31,8 @@ type sqlInputsSelector struct { db *gorm.DB } -// NewSelector creates a new instance of Selector. -func NewSelector(db *gorm.DB, feeUnit bsv.FeeUnit) Selector { +// NewUTXOSelector creates a new instance of UTXOSelector. +func NewUTXOSelector(db *gorm.DB, feeUnit bsv.FeeUnit) UTXOSelector { return &sqlInputsSelector{ db: db, feeUnit: feeUnit, diff --git a/engine/transaction/outlines/internal/inputs/selector_example_test.go b/engine/transaction/outlines/utxo/internal/sql/selector_example_test.go similarity index 85% rename from engine/transaction/outlines/internal/inputs/selector_example_test.go rename to engine/transaction/outlines/utxo/internal/sql/selector_example_test.go index 49a55ff2f..7741bec75 100644 --- a/engine/transaction/outlines/internal/inputs/selector_example_test.go +++ b/engine/transaction/outlines/utxo/internal/sql/selector_example_test.go @@ -1,4 +1,4 @@ -package inputs +package sql import ( "fmt" @@ -10,8 +10,8 @@ import ( "gorm.io/gorm" ) -// ExampleSelector_buildQueryForInputs_sqlite demonstrates what would be the query used to select inputs for a transaction. -func ExampleSelector_buildQueryForInputs_sqlite() { +// ExampleUTXOSelector_buildQueryForInputs_sqlite demonstrates what would be the query used to select inputs for a transaction. +func ExampleUTXOSelector_buildQueryForInputs_sqlite() { db := tgorm.GormDBForPrintingSQL(tgorm.SQLite) // and: @@ -28,8 +28,8 @@ func ExampleSelector_buildQueryForInputs_sqlite() { // Output: SELECT * FROM `xapi_user_utxos` WHERE (tx_id, vout) in (SELECT tx_id,vout FROM (SELECT tx_id,vout,change,min(case when change >= 0 then change end) over () as min_change FROM (SELECT tx_id,vout,case when remaining_value - fee_no_change_output <= 0 then remaining_value - fee_no_change_output else remaining_value - fee_with_change_output end as change FROM (SELECT `tx_id`,`vout`,sum(satoshis) over (order by touched_at ASC, created_at ASC, tx_id ASC, vout ASC) - 1 as remaining_value,ceil((sum(estimated_input_size) over (order by touched_at ASC, created_at ASC, tx_id ASC, vout ASC) + 10) / cast(1000 as float)) * 1 as fee_no_change_output,ceil((sum(estimated_input_size) over (order by touched_at ASC, created_at ASC, tx_id ASC, vout ASC) + 10 + 34) / cast(1000 as float)) * 1 as fee_with_change_output FROM `xapi_user_utxos` WHERE user_id = "someuserid") as utxo) as utxoWithChange) as utxoWithMinChange WHERE change <= min_change) } -// ExampleSelector_buildQueryForInputs_postgresql demonstrates what would be the query used to select inputs for a transaction. -func ExampleSelector_buildQueryForInputs_postgresql() { +// ExampleUTXOSelector_buildQueryForInputs_postgresql demonstrates what would be the query used to select inputs for a transaction. +func ExampleUTXOSelector_buildQueryForInputs_postgresql() { db := tgorm.GormDBForPrintingSQL(tgorm.PostgreSQL) // and: @@ -46,8 +46,8 @@ func ExampleSelector_buildQueryForInputs_postgresql() { // Output: SELECT * FROM "xapi_user_utxos" WHERE (tx_id, vout) in (SELECT tx_id,vout FROM (SELECT tx_id,vout,change,min(case when change >= 0 then change end) over () as min_change FROM (SELECT tx_id,vout,case when remaining_value - fee_no_change_output <= 0 then remaining_value - fee_no_change_output else remaining_value - fee_with_change_output end as change FROM (SELECT "tx_id","vout",sum(satoshis) over (order by touched_at ASC, created_at ASC, tx_id ASC, vout ASC) - 1 as remaining_value,ceil((sum(estimated_input_size) over (order by touched_at ASC, created_at ASC, tx_id ASC, vout ASC) + 10) / cast(1000 as float)) * 1 as fee_no_change_output,ceil((sum(estimated_input_size) over (order by touched_at ASC, created_at ASC, tx_id ASC, vout ASC) + 10 + 34) / cast(1000 as float)) * 1 as fee_with_change_output FROM "xapi_user_utxos" WHERE user_id = 'someuserid') as utxo) as utxoWithChange) as utxoWithMinChange WHERE change <= min_change) } -// ExampleSelector_buildUpdateTouchedAtQuery_sqlite demonstrates what would be the SQL statement used to update inputs after selecting them. -func ExampleSelector_buildUpdateTouchedAtQuery_sqlite() { +// ExampleUTXOSelector_buildUpdateTouchedAtQuery_sqlite demonstrates what would be the SQL statement used to update inputs after selecting them. +func ExampleUTXOSelector_buildUpdateTouchedAtQuery_sqlite() { db := tgorm.GormDBForPrintingSQL(tgorm.SQLite) selector := givenInputsSelector(db) @@ -69,8 +69,8 @@ func ExampleSelector_buildUpdateTouchedAtQuery_sqlite() { // Output: UPDATE `xapi_user_utxos` SET `touched_at`="2006-02-01 15:04:05" WHERE (tx_id, vout) in (("tx_id_1",0),("tx_id_1",1),("tx_id_2",0)) } -// ExampleSelector_buildUpdateTouchedAtQuery_postgres demonstrates what would be the SQL statement used to update inputs after selecting them. -func ExampleSelector_buildUpdateTouchedAtQuery_postgres() { +// ExampleUTXOSelector_buildUpdateTouchedAtQuery_postgres demonstrates what would be the SQL statement used to update inputs after selecting them. +func ExampleUTXOSelector_buildUpdateTouchedAtQuery_postgres() { db := tgorm.GormDBForPrintingSQL(tgorm.PostgreSQL) selector := givenInputsSelector(db) @@ -93,7 +93,7 @@ func ExampleSelector_buildUpdateTouchedAtQuery_postgres() { } func givenInputsSelector(db *gorm.DB) *sqlInputsSelector { - selector := NewSelector(db, bsv.FeeUnit{Satoshis: 1, Bytes: 1000}) + selector := NewUTXOSelector(db, bsv.FeeUnit{Satoshis: 1, Bytes: 1000}) result, ok := selector.(*sqlInputsSelector) if !ok { panic("failed to cast selector to sqlInputsSelector") diff --git a/engine/transaction/outlines/internal/inputs/selector_test.go b/engine/transaction/outlines/utxo/internal/sql/selector_test.go similarity index 97% rename from engine/transaction/outlines/internal/inputs/selector_test.go rename to engine/transaction/outlines/utxo/internal/sql/selector_test.go index 7f29cd0c9..21c84163a 100644 --- a/engine/transaction/outlines/internal/inputs/selector_test.go +++ b/engine/transaction/outlines/utxo/internal/sql/selector_test.go @@ -1,4 +1,4 @@ -package inputs_test +package sql_test import ( "context" @@ -6,7 +6,7 @@ import ( "github.com/bitcoin-sv/spv-wallet/engine/database" "github.com/bitcoin-sv/spv-wallet/engine/tester/fixtures" - "github.com/bitcoin-sv/spv-wallet/engine/transaction/outlines/internal/inputs/testabilities" + "github.com/bitcoin-sv/spv-wallet/engine/transaction/outlines/utxo/internal/sql/testabilities" "github.com/bitcoin-sv/spv-wallet/models/bsv" "github.com/stretchr/testify/require" ) diff --git a/engine/transaction/outlines/internal/inputs/testabilities/assertions_inputs_selector.go b/engine/transaction/outlines/utxo/internal/sql/testabilities/assertions_inputs_selector.go similarity index 100% rename from engine/transaction/outlines/internal/inputs/testabilities/assertions_inputs_selector.go rename to engine/transaction/outlines/utxo/internal/sql/testabilities/assertions_inputs_selector.go diff --git a/engine/transaction/outlines/internal/inputs/testabilities/fixture_inputs_selector.go b/engine/transaction/outlines/utxo/internal/sql/testabilities/fixture_inputs_selector.go similarity index 73% rename from engine/transaction/outlines/internal/inputs/testabilities/fixture_inputs_selector.go rename to engine/transaction/outlines/utxo/internal/sql/testabilities/fixture_inputs_selector.go index fbee0c952..2c0f3cfcb 100644 --- a/engine/transaction/outlines/internal/inputs/testabilities/fixture_inputs_selector.go +++ b/engine/transaction/outlines/utxo/internal/sql/testabilities/fixture_inputs_selector.go @@ -6,13 +6,13 @@ import ( "github.com/bitcoin-sv/spv-wallet/engine/database/testabilities" testengine "github.com/bitcoin-sv/spv-wallet/engine/testabilities" "github.com/bitcoin-sv/spv-wallet/engine/tester/fixtures" - "github.com/bitcoin-sv/spv-wallet/engine/transaction/outlines/internal/inputs" + "github.com/bitcoin-sv/spv-wallet/engine/transaction/outlines/utxo/internal/sql" "gorm.io/gorm" ) type InputsSelectorFixture interface { testabilities.DatabaseFixture - NewInputSelector() inputs.Selector + NewInputSelector() sql.UTXOSelector } type inputsSelectorFixture struct { @@ -28,6 +28,6 @@ func newFixture(t testing.TB) (InputsSelectorFixture, func()) { }, cleanup } -func (i *inputsSelectorFixture) NewInputSelector() inputs.Selector { - return inputs.NewSelector(i.db, fixtures.DefaultFeeUnit) +func (i *inputsSelectorFixture) NewInputSelector() sql.UTXOSelector { + return sql.NewUTXOSelector(i.db, fixtures.DefaultFeeUnit) } diff --git a/engine/transaction/outlines/internal/inputs/testabilities/testability_inputs_selector.go b/engine/transaction/outlines/utxo/internal/sql/testabilities/testability_inputs_selector.go similarity index 100% rename from engine/transaction/outlines/internal/inputs/testabilities/testability_inputs_selector.go rename to engine/transaction/outlines/utxo/internal/sql/testabilities/testability_inputs_selector.go