Skip to content

Commit

Permalink
feat(SPV-1121): adjust op_return recording to domain services approach (
Browse files Browse the repository at this point in the history
  • Loading branch information
chris-4chain authored Jan 29, 2025
1 parent 9176446 commit 924bdb9
Show file tree
Hide file tree
Showing 39 changed files with 761 additions and 233 deletions.
17 changes: 13 additions & 4 deletions actions/testabilities/fixture_spvwallet_application.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ type SPVWalletApplicationFixture interface {

// ARC creates a new test fixture for ARC
ARC() ARCFixture

Faucet(user fixtures.User) testengine.FaucetFixture
}

type BlockHeadersServiceFixture interface {
Expand Down Expand Up @@ -60,10 +62,11 @@ type SPVWalletHttpClientFixture interface {
}

type appFixture struct {
engineFixture testengine.EngineFixture
t testing.TB
logger zerolog.Logger
server testServer
engineWithConfig testengine.EngineWithConfig
engineFixture testengine.EngineFixture
t testing.TB
logger zerolog.Logger
server testServer
}

func Given(t testing.TB) SPVWalletApplicationFixture {
Expand Down Expand Up @@ -93,6 +96,8 @@ func (f *appFixture) StartedSPVWalletWithConfiguration(opts ...testengine.Config
s := server.NewServer(&engineWithConfig.Config, engineWithConfig.Engine, f.logger)
f.server.handlers = s.Handlers()

f.engineWithConfig = engineWithConfig

return cleanup
}

Expand Down Expand Up @@ -132,3 +137,7 @@ func (f *appFixture) BHS() BlockHeadersServiceFixture {
func (f *appFixture) ARC() ARCFixture {
return f.engineFixture.ARC()
}

func (f *appFixture) Faucet(user fixtures.User) testengine.FaucetFixture {
return f.engineFixture.Faucet(user)
}
13 changes: 13 additions & 0 deletions actions/v2/transactions/internal/mapping/recorded_outline.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package mapping

import (
"github.com/bitcoin-sv/spv-wallet/engine/v2/transaction/txmodels"
"github.com/bitcoin-sv/spv-wallet/models/response"
)

// RecordedOutline maps domain RecordedOutline to response.RecordedOutline.
func RecordedOutline(r *txmodels.RecordedOutline) response.RecordedOutline {
return response.RecordedOutline{
TxID: r.TxID,
}
}
37 changes: 37 additions & 0 deletions actions/v2/transactions/internal/mapping/transaction_outline.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package mapping

import (
"maps"

"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"
)

// TransactionOutline maps request's AnnotatedTransaction to outlines.Transaction.
func TransactionOutline(req *request.AnnotatedTransaction) *outlines.Transaction {
return &outlines.Transaction{
BEEF: req.BEEF,
Annotations: transaction.Annotations{
Outputs: maps.Collect(func(yield func(int, *transaction.OutputAnnotation) bool) {
if req.Annotations == nil || len(req.Annotations.Outputs) == 0 {
return
}
for index, output := range req.Annotations.Outputs {
var paymail *transaction.PaymailAnnotation
if output.Paymail != nil {
paymail = &transaction.PaymailAnnotation{
Receiver: output.Paymail.Receiver,
Reference: output.Paymail.Reference,
Sender: output.Paymail.Sender,
}
}
yield(index, &transaction.OutputAnnotation{
Bucket: output.Bucket,
Paymail: paymail,
})
}
}),
},
}
}
12 changes: 7 additions & 5 deletions actions/v2/transactions/outlines_record.go
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
package transactions

import (
"github.com/bitcoin-sv/spv-wallet/actions/v2/transactions/internal/mapping/annotatedtx"
"github.com/bitcoin-sv/spv-wallet/actions/v2/transactions/internal/mapping"
"github.com/bitcoin-sv/spv-wallet/engine/spverrors"
"github.com/bitcoin-sv/spv-wallet/models/request"
"github.com/bitcoin-sv/spv-wallet/server/reqctx"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
)

func transactionRecordOutline(c *gin.Context, userContext *reqctx.UserContext) {
func recordOutline(c *gin.Context, userContext *reqctx.UserContext) {
logger := reqctx.Logger(c)

var requestBody annotatedtx.Request
var requestBody request.AnnotatedTransaction
err := c.ShouldBindWith(&requestBody, binding.JSON)
if err != nil {
spverrors.ErrorResponse(c, spverrors.ErrCannotBindRequest.Wrap(err), logger)
Expand All @@ -25,10 +26,11 @@ func transactionRecordOutline(c *gin.Context, userContext *reqctx.UserContext) {
}

recordService := reqctx.Engine(c).TransactionRecordService()
if err = recordService.RecordTransactionOutline(c, userID, requestBody.ToEngine()); err != nil {
recorded, err := recordService.RecordTransactionOutline(c, userID, mapping.TransactionOutline(&requestBody))
if err != nil {
spverrors.ErrorResponse(c, err, logger)
return
}

c.JSON(200, nil)
c.JSON(201, mapping.RecordedOutline(recorded))
}
64 changes: 59 additions & 5 deletions actions/v2/transactions/outlines_record_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,17 +22,30 @@ func givenTXWithOpReturn(t *testing.T) fixtures.GivenTXSpec {
}

func TestOutlinesRecordOpReturn(t *testing.T) {
// given:
givenForAllTests := testabilities.Given(t)
cleanup := givenForAllTests.StartedSPVWalletWithConfiguration(
testengine.WithV2(),
)
defer cleanup()

// and:
ownedTransaction := givenForAllTests.Faucet(fixtures.Sender).TopUp(1000)

// and:
txSpec := fixtures.GivenTX(t).
WithSender(fixtures.Sender).
WithInputFromUTXO(ownedTransaction.TX(), 0).
WithOPReturn(dataOfOpReturnTx)

t.Run("Record op_return data", func(t *testing.T) {
// given:
given, then := testabilities.New(t)
cleanup := given.StartedSPVWalletWithConfiguration(testengine.WithV2())
defer cleanup()
given, then := testabilities.NewOf(givenForAllTests, t)

// and:
client := given.HttpClient().ForUser()

// and:
txSpec := givenTXWithOpReturn(t)
request := `{
"beef": "` + txSpec.BEEF() + `",
"annotations": {
Expand All @@ -57,7 +70,48 @@ func TestOutlinesRecordOpReturn(t *testing.T) {
Post(transactionsOutlinesRecordURL)

// then:
then.Response(res).IsOK()
then.Response(res).
HasStatus(201).
WithJSONMatching(`{
"txID": "{{ .txID }}"
}`, map[string]any{
"txID": txSpec.ID(),
})
})

t.Run("get operations", func(t *testing.T) {
// given:
given, then := testabilities.NewOf(givenForAllTests, t)

// and:
client := given.HttpClient().ForUser()

// when:
res, _ := client.R().Get("/api/v2/operations/search")

// then:
then.Response(res).IsOK().WithJSONMatching(`{
"content": [
{
"txID": "{{ .txID }}",
"createdAt": "{{ matchTimestamp }}",
"value": {{ .value }},
"type": "outgoing",
"counterparty": "{{ .sender }}"
},
{{ anything }}
],
"page": {
"number": 1,
"size": 2,
"totalElements": 2,
"totalPages": 1
}
}`, map[string]any{
"value": -1000,
"txID": txSpec.ID(),
"sender": "",
})
})
}

Expand Down
15 changes: 8 additions & 7 deletions actions/v2/transactions/routes.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
package transactions

import "github.com/bitcoin-sv/spv-wallet/server/handlers"
import (
"github.com/bitcoin-sv/spv-wallet/server/handlers"
routes "github.com/bitcoin-sv/spv-wallet/server/handlers"
)

// RegisterRoutes creates the specific package routes
func RegisterRoutes(handlersManager *handlers.Manager) {
if handlersManager.GetFeatureFlags().V2 {
group := handlersManager.Group(handlers.GroupAPIV2, "/transactions")
group.POST("/outlines", handlers.AsUser(transactionOutlines))
group.POST("", handlers.AsUser(transactionRecordOutline))
}
func RegisterRoutes(handlersManager *routes.Manager) {
group := handlersManager.Group(handlers.GroupAPIV2, "/transactions")
group.POST("/outlines", handlers.AsUser(transactionOutlines))
group.POST("", handlers.AsUser(recordOutline))
}
63 changes: 63 additions & 0 deletions engine/testabilities/faucet_fixture.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package testabilities

import (
"context"
"testing"

"github.com/bitcoin-sv/spv-wallet/engine"
"github.com/bitcoin-sv/spv-wallet/engine/tester/fixtures"
"github.com/bitcoin-sv/spv-wallet/engine/v2/transaction/txmodels"
"github.com/bitcoin-sv/spv-wallet/models/bsv"
"github.com/stretchr/testify/assert"
)

type faucetFixture struct {
engine engine.ClientInterface
user fixtures.User
t testing.TB
assert *assert.Assertions
arc ARCFixture
bhs BlockHeadersServiceFixture
}

func (f *faucetFixture) TopUp(satoshis bsv.Satoshis) fixtures.GivenTXSpec {
f.t.Helper()

txSpec := fixtures.GivenTX(f.t).
WithSender(fixtures.ExternalFaucet).
WithInput(uint64(satoshis + 1)).
WithRecipient(f.user).
WithP2PKHOutput(uint64(satoshis))

operation := txmodels.NewOperation{
UserID: f.user.ID(),

Type: "incoming",
Value: int64(satoshis), //nolint:gosec // This is a test fixture, values won't exceed int64

Transaction: &txmodels.NewTransaction{
ID: txSpec.ID(),
TxStatus: txmodels.TxStatusMined,
Outputs: []txmodels.NewOutput{
txmodels.NewOutputForP2PKH(
bsv.Outpoint{TxID: txSpec.ID(), Vout: 0},
f.user.ID(),
satoshis,
nil,
),
},
},
}

err := f.engine.Repositories().Operations.SaveAll(context.Background(), func(yield func(*txmodels.NewOperation) bool) {
yield(&operation)
})
f.assert.NoError(err)

// Additional check - assertion if the top-up operation was saved correctly
balance, err := f.engine.Repositories().Users.GetBalance(context.Background(), f.user.ID(), "bsv")
f.assert.NoError(err)
f.assert.GreaterOrEqual(balance, satoshis)

return txSpec
}
21 changes: 21 additions & 0 deletions engine/testabilities/fixture_engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,11 @@ import (
"github.com/bitcoin-sv/spv-wallet/engine/tester/paymailmock"
"github.com/bitcoin-sv/spv-wallet/engine/v2/paymails/paymailsmodels"
"github.com/bitcoin-sv/spv-wallet/engine/v2/users/usersmodels"
"github.com/bitcoin-sv/spv-wallet/models/bsv"
"github.com/go-resty/resty/v2"
"github.com/jarcoal/httpmock"
"github.com/rs/zerolog"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

Expand All @@ -42,6 +44,14 @@ type EngineFixture interface {

// ARC creates a new test fixture for ARC
ARC() ARCFixture

// Faucet creates a new test fixture for Faucet
Faucet(user fixtures.User) FaucetFixture
}

// FaucetFixture is a test fixture for the faucet service
type FaucetFixture interface {
TopUp(satoshis bsv.Satoshis) fixtures.GivenTXSpec
}

type EngineWithConfig struct {
Expand Down Expand Up @@ -122,6 +132,17 @@ func (f *engineFixture) ConfigForTests(opts ...ConfigOpts) *config.AppConfig {
return configuration
}

func (f *engineFixture) Faucet(user fixtures.User) FaucetFixture {
return &faucetFixture{
engine: f.engine,
user: user,
t: f.t,
assert: assert.New(f.t),
arc: f.ARC(),
bhs: f.BHS(),
}
}

// prepareDBConfigForTests creates a new connection that will be used as connection for engine
func (f *engineFixture) prepareDBConfigForTests() {
require.Equal(f.t, datastore.SQLite, f.config.Db.Datastore.Engine, "Other datastore engines are not supported in tests (yet)")
Expand Down
Loading

0 comments on commit 924bdb9

Please sign in to comment.