Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(SPV-1125) finalize the story for op_return recording #877

Merged
merged 7 commits into from
Jan 30, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ dist
coverage.txt

# Development data dir
data/
/data/

# Configuration files
.env.config
Expand Down
41 changes: 41 additions & 0 deletions actions/v2/data/get.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package data

import (
"net/http"

"github.com/bitcoin-sv/spv-wallet/actions/v2/data/internal/mapping"
"github.com/bitcoin-sv/spv-wallet/engine/spverrors"
"github.com/bitcoin-sv/spv-wallet/models/bsv"
"github.com/bitcoin-sv/spv-wallet/server/reqctx"
"github.com/gin-gonic/gin"
)

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

userID, err := userContext.ShouldGetUserID()
if err != nil {
spverrors.AbortWithErrorResponse(c, err, logger)
return
}

dataID := c.Param("id")
_, err = bsv.OutpointFromString(dataID)
if err != nil {
spverrors.ErrorResponse(c, spverrors.ErrInvalidDataID.Wrap(err), logger)
return
}

data, err := reqctx.Engine(c).DataService().FindForUser(c.Request.Context(), dataID, userID)
if err != nil {
spverrors.ErrorResponse(c, err, logger)
return
}

if data == nil {
spverrors.ErrorResponse(c, spverrors.ErrDataNotFound, logger)
return
}

c.JSON(http.StatusOK, mapping.DataResponse(data))
}
107 changes: 107 additions & 0 deletions actions/v2/data/get_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package data_test

import (
"testing"

"github.com/bitcoin-sv/spv-wallet/actions/testabilities"
"github.com/bitcoin-sv/spv-wallet/actions/testabilities/apierror"
testengine "github.com/bitcoin-sv/spv-wallet/engine/testabilities"
"github.com/bitcoin-sv/spv-wallet/engine/tester/fixtures"
"github.com/bitcoin-sv/spv-wallet/models/bsv"
)

// NOTE: More complex and real-world test case can be found in outlines_record_test.go
func TestGetData(t *testing.T) {
// given:
given, then := testabilities.New(t)
cleanup := given.StartedSPVWalletWithConfiguration(
testengine.WithV2(),
)
defer cleanup()

// and:
dataToStore := "hello world"

// and:
_, dataID := given.Faucet(fixtures.Sender).StoreData(dataToStore)

// and:
client := given.HttpClient().ForGivenUser(fixtures.Sender)

// when:
res, _ := client.R().
Get("/api/v2/data/" + dataID)

// then:
then.Response(res).
IsOK().WithJSONMatching(`{
"id": "{{ .outpoint }}",
"blob": "{{ .blob }}"
}`, map[string]any{
"outpoint": dataID,
"blob": dataToStore,
})
}

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

// and
mockTx := fixtures.GivenTX(t).WithInput(1).WithP2PKHOutput(1)
mockOutpoint := bsv.Outpoint{
TxID: mockTx.ID(),
Vout: 0,
}

t.Run("try to get data as admin", func(t *testing.T) {
// given:
given, then := testabilities.NewOf(givenForAllTests, t)

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

// when:
res, _ := client.R().Get("/api/v2/data/" + mockOutpoint.String())

// then:
then.Response(res).IsUnauthorizedForAdmin()
})

t.Run("try to get data as anonymous", func(t *testing.T) {
// given:
given, then := testabilities.NewOf(givenForAllTests, t)

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

// when:
res, _ := client.R().Get("/api/v2/data/" + mockOutpoint.String())

// then:
then.Response(res).IsUnauthorized()
})

t.Run("try to get data with wrong id", func(t *testing.T) {
// given:
given, then := testabilities.NewOf(givenForAllTests, t)

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

// and:
wrongID := "wrong_id" //doesn't match the outpoint format "<txID>-<vout>"

// when:
res, _ := client.R().Get("/api/v2/data/" + wrongID)

// then:
then.Response(res).HasStatus(400).WithJSONf(
apierror.ExpectedJSON("error-invalid-data-id", "invalid data id"),
)
})
}
14 changes: 14 additions & 0 deletions actions/v2/data/internal/mapping/data.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package mapping

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

// DataResponse maps a domain data model to a response model
func DataResponse(data *datamodels.Data) response.Data {
return response.Data{
ID: data.ID(),
Blob: string(data.Blob),
}
}
12 changes: 12 additions & 0 deletions actions/v2/data/routes.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package data

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 *routes.Manager) {
group := handlersManager.Group(routes.GroupAPIV2, "/data")
group.GET("/:id", handlers.AsUser(get))
dorzepowski marked this conversation as resolved.
Show resolved Hide resolved
}
6 changes: 3 additions & 3 deletions actions/v2/operations/search.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,14 @@ import (
)

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

userID, err := userContext.ShouldGetUserID()
if err != nil {
spverrors.ErrorResponse(c, spverrors.ErrCannotBindRequest, reqctx.Logger(c))
spverrors.AbortWithErrorResponse(c, err, logger)
dorzepowski marked this conversation as resolved.
Show resolved Hide resolved
return
}

logger := reqctx.Logger(c)

searchParams, err := query.ParseSearchParams[struct{}](c)
if err != nil {
spverrors.ErrorResponse(c, spverrors.ErrCannotParseQueryParams.WithTrace(err), logger)
Expand Down
2 changes: 2 additions & 0 deletions actions/v2/register.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package v2

import (
"github.com/bitcoin-sv/spv-wallet/actions/v2/admin"
"github.com/bitcoin-sv/spv-wallet/actions/v2/data"
"github.com/bitcoin-sv/spv-wallet/actions/v2/operations"
"github.com/bitcoin-sv/spv-wallet/actions/v2/transactions"
"github.com/bitcoin-sv/spv-wallet/actions/v2/users"
Expand All @@ -13,6 +14,7 @@ func Register(handlersManager *handlers.Manager) {
users.RegisterRoutes(handlersManager)
operations.RegisterRoutes(handlersManager)
transactions.RegisterRoutes(handlersManager)
data.RegisterRoutes(handlersManager)

admin.Register(handlersManager)
}
26 changes: 26 additions & 0 deletions actions/v2/transactions/outlines_record_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
chainmodels "github.com/bitcoin-sv/spv-wallet/engine/chain/models"
testengine "github.com/bitcoin-sv/spv-wallet/engine/testabilities"
"github.com/bitcoin-sv/spv-wallet/engine/tester/fixtures"
"github.com/bitcoin-sv/spv-wallet/models/bsv"
)

const (
Expand Down Expand Up @@ -113,6 +114,31 @@ func TestOutlinesRecordOpReturn(t *testing.T) {
"sender": "",
})
})

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

// and:
outpoint := bsv.Outpoint{TxID: txSpec.ID(), Vout: 0}

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

// when:
res, _ := client.R().
Get("/api/v2/data/" + outpoint.String())

// then:
then.Response(res).
IsOK().WithJSONMatching(`{
"id": "{{ .outpoint }}",
"blob": "{{ .blob }}"
}`, map[string]any{
"outpoint": outpoint.String(),
"blob": dataOfOpReturnTx,
})
})
}

func TestOutlinesRecordOpReturnErrorCases(t *testing.T) {
Expand Down
8 changes: 8 additions & 0 deletions engine/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"github.com/bitcoin-sv/spv-wallet/engine/spverrors"
"github.com/bitcoin-sv/spv-wallet/engine/taskmanager"
"github.com/bitcoin-sv/spv-wallet/engine/v2/addresses"
"github.com/bitcoin-sv/spv-wallet/engine/v2/data"
"github.com/bitcoin-sv/spv-wallet/engine/v2/database/repository"
"github.com/bitcoin-sv/spv-wallet/engine/v2/paymails"
"github.com/bitcoin-sv/spv-wallet/engine/v2/transaction/outlines"
Expand Down Expand Up @@ -62,6 +63,7 @@ type (
users *users.Service // User domain service
paymails *paymails.Service // Paymail domain service
addresses *addresses.Service
data *data.Service
}

// cacheStoreOptions holds the cache configuration and client
Expand Down Expand Up @@ -155,6 +157,7 @@ func NewClient(ctx context.Context, opts ...ClientOps) (ClientInterface, error)
client.loadUsersService()
client.loadPaymailsService()
client.loadAddressesService()
client.loadDataService()

// Load the Paymail client and service (if does not exist)
if err = client.loadPaymailComponents(); err != nil {
Expand Down Expand Up @@ -360,3 +363,8 @@ func (c *Client) PaymailsService() *paymails.Service {
func (c *Client) AddressesService() *addresses.Service {
return c.options.addresses
}

// DataService will return the data domain service
func (c *Client) DataService() *data.Service {
return c.options.data
}
7 changes: 7 additions & 0 deletions engine/client_internal.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"github.com/bitcoin-sv/spv-wallet/engine/spverrors"
"github.com/bitcoin-sv/spv-wallet/engine/taskmanager"
"github.com/bitcoin-sv/spv-wallet/engine/v2/addresses"
"github.com/bitcoin-sv/spv-wallet/engine/v2/data"
"github.com/bitcoin-sv/spv-wallet/engine/v2/database/repository"
"github.com/bitcoin-sv/spv-wallet/engine/v2/paymails"
paymailprovider "github.com/bitcoin-sv/spv-wallet/engine/v2/paymailserver"
Expand Down Expand Up @@ -211,6 +212,12 @@ func (c *Client) loadAddressesService() {
}
}

func (c *Client) loadDataService() {
if c.options.data == nil {
c.options.data = data.NewService(c.Repositories().Data)
}
}

func (c *Client) loadChainService() {
if c.options.chainService == nil {
logger := c.Logger().With().Str("subservice", "chain").Logger()
Expand Down
2 changes: 2 additions & 0 deletions engine/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
paymailclient "github.com/bitcoin-sv/spv-wallet/engine/paymail"
"github.com/bitcoin-sv/spv-wallet/engine/taskmanager"
"github.com/bitcoin-sv/spv-wallet/engine/v2/addresses"
"github.com/bitcoin-sv/spv-wallet/engine/v2/data"
"github.com/bitcoin-sv/spv-wallet/engine/v2/database/repository"
"github.com/bitcoin-sv/spv-wallet/engine/v2/paymails"
"github.com/bitcoin-sv/spv-wallet/engine/v2/transaction/outlines"
Expand Down Expand Up @@ -162,6 +163,7 @@ type V2 interface {
UsersService() *users.Service
PaymailsService() *paymails.Service
AddressesService() *addresses.Service
DataService() *data.Service
}

// ClientInterface is the client (spv wallet engine) interface comprised of all services/actions
Expand Down
9 changes: 6 additions & 3 deletions engine/spverrors/definitions.go
Original file line number Diff line number Diff line change
Expand Up @@ -441,9 +441,6 @@ var ErrBroadcast = models.SPVError{Message: "broadcast error", StatusCode: 500,

// ////////////////////////////////// CONVERSION ERRORS

// ErrInvalidUint is when uint value is invalid
var ErrInvalidUint = models.SPVError{Message: "invalid uint value", StatusCode: 500, Code: "error-invalid-uint-value"}

// ErrInvalidInt is when uint value is invalid
var ErrInvalidInt = models.SPVError{Message: "invalid int value", StatusCode: 500, Code: "error-invalid-int-value"}

Expand All @@ -464,3 +461,9 @@ var ErrInvalidUint64 = models.SPVError{Message: "invalid uint64 value", StatusCo

// ErrMissingXPubID is when xpub_id is missing
var ErrMissingXPubID = models.SPVError{Message: "missing xpub_id", StatusCode: 400, Code: "error-missing-xpub-id"}

// ErrDataNotFound is when data record cannot be found
var ErrDataNotFound = models.SPVError{Message: "data not found", StatusCode: 404, Code: "error-data-not-found"}

// ErrInvalidDataID is when data id is invalid
var ErrInvalidDataID = models.SPVError{Message: "invalid data id", StatusCode: 400, Code: "error-invalid-data-id"}
1 change: 1 addition & 0 deletions engine/testabilities/fixture_engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ type EngineFixture interface {
// FaucetFixture is a test fixture for the faucet service
type FaucetFixture interface {
TopUp(satoshis bsv.Satoshis) fixtures.GivenTXSpec
StoreData(data string) (fixtures.GivenTXSpec, string)
}

type EngineWithConfig struct {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,40 @@ func (f *faucetFixture) TopUp(satoshis bsv.Satoshis) fixtures.GivenTXSpec {

return txSpec
}

func (f *faucetFixture) StoreData(data string) (fixtures.GivenTXSpec, string) {
f.t.Helper()

txSpec := fixtures.GivenTX(f.t).
WithSender(fixtures.ExternalFaucet).
WithInput(uint64(1000)).
WithOPReturn(data)

outpoint := bsv.Outpoint{TxID: txSpec.ID(), Vout: 0}

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

Type: "data",
Value: 0,

Transaction: &txmodels.NewTransaction{
ID: txSpec.ID(),
TxStatus: txmodels.TxStatusMined,
Outputs: []txmodels.NewOutput{
txmodels.NewOutputForData(
outpoint,
f.user.ID(),
[]byte(data),
),
},
},
}

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

return txSpec, outpoint.String()
}
Loading
Loading