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

Generic TSS #83

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
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
77 changes: 59 additions & 18 deletions internal/serve/httphandler/tss_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,67 +5,110 @@ import (

"github.com/stellar/go/support/log"
"github.com/stellar/go/support/render/httpjson"
"github.com/stellar/go/txnbuild"
"github.com/stellar/wallet-backend/internal/apptracker"
"github.com/stellar/wallet-backend/internal/serve/httperror"
"github.com/stellar/wallet-backend/internal/tss"
"github.com/stellar/wallet-backend/internal/tss/router"
tssservices "github.com/stellar/wallet-backend/internal/tss/services"
"github.com/stellar/wallet-backend/internal/tss/store"
"github.com/stellar/wallet-backend/internal/tss/utils"
)

type TSSHandler struct {
Router router.Router
Store store.Store
AppTracker apptracker.AppTracker
NetworkPassphrase string
Router router.Router
Store store.Store
AppTracker apptracker.AppTracker
NetworkPassphrase string
TransactionService tssservices.TransactionService
}

type Transaction struct {
Operations []string `json:"operations" validate:"required"`
TimeOut int64 `json:"timeout" validate:"required"`
}

type TransactionSubmissionRequest struct {
WebhookURL string `json:"webhook" validate:"required"`
type BuildTransactionsRequest struct {
Transactions []Transaction `json:"transactions" validate:"required,gt=0"`
}

type BuildTransactionsResponse struct {
TransactionXDRs []string `json:"transactionxdrs"`
}

type TransactionSubmissionRequest struct {
WebhookURL string `json:"webhook" validate:"required"`
Transactions []string `json:"transactions" validate:"required,gt=0"`
FeeBump bool `json:"feebump"`
}

type TransactionSubmissionResponse struct {
TransactionHashes []string `json:"transactionhashes"`
}

func (t *TSSHandler) SubmitTransactions(w http.ResponseWriter, r *http.Request) {
func (t *TSSHandler) BuildTransactions(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()

var reqParams TransactionSubmissionRequest
var reqParams BuildTransactionsRequest
httpErr := DecodeJSONAndValidate(ctx, r, &reqParams, t.AppTracker)
if httpErr != nil {
httpErr.Render(w)
return
}
var transactionHashes []string
var payloads []tss.Payload
var transactionXDRs []string
for _, transaction := range reqParams.Transactions {
tx, err := utils.BuildOriginalTransaction(transaction.Operations)
ops, err := utils.BuildOperations(transaction.Operations)
if err != nil {
httperror.BadRequest("bad operation xdr", nil).Render(w)
return
}
txHash, err := tx.HashHex(t.NetworkPassphrase)
tx, err := t.TransactionService.BuildAndSignTransactionWithChannelAccount(ctx, ops, transaction.TimeOut)
if err != nil {
httperror.InternalServerError(ctx, "unable to hashhex transaction", err, nil, t.AppTracker).Render(w)
httperror.InternalServerError(ctx, "unable to build transaction", err, nil, t.AppTracker).Render(w)
return
}

txXDR, err := tx.Base64()
txXdrStr, err := tx.Base64()
if err != nil {
httperror.InternalServerError(ctx, "unable to base64 transaction", err, nil, t.AppTracker).Render(w)
return
}
transactionXDRs = append(transactionXDRs, txXdrStr)
}
httpjson.Render(w, BuildTransactionsResponse{
TransactionXDRs: transactionXDRs,
}, httpjson.JSON)
}

func (t *TSSHandler) SubmitTransactions(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var reqParams TransactionSubmissionRequest
httpErr := DecodeJSONAndValidate(ctx, r, &reqParams, t.AppTracker)
if httpErr != nil {
httpErr.Render(w)
return
}
var transactionHashes []string
var payloads []tss.Payload
for _, txXDR := range reqParams.Transactions {
genericTx, err := txnbuild.TransactionFromXDR(txXDR)
if err != nil {
httperror.BadRequest("bad transaction xdr", nil).Render(w)
return
}
tx, txEmpty := genericTx.Transaction()
if !txEmpty {
httperror.BadRequest("bad transaction xdr", nil).Render(w)
return
}
txHash, err := tx.HashHex(t.NetworkPassphrase)
if err != nil {
httperror.InternalServerError(ctx, "unable to hashhex transaction", err, nil, t.AppTracker).Render(w)
return
}
payload := tss.Payload{
TransactionHash: txHash,
TransactionXDR: txXDR,
WebhookURL: reqParams.WebhookURL,
FeeBump: reqParams.FeeBump,
}

payloads = append(payloads, payload)
Expand All @@ -80,9 +123,7 @@ func (t *TSSHandler) SubmitTransactions(w http.ResponseWriter, r *http.Request)
if err != nil {
log.Errorf("unable to route payload: %v", err)
}

}

}

type GetTransactionRequest struct {
Expand Down
151 changes: 121 additions & 30 deletions internal/serve/httphandler/tss_handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
Expand All @@ -21,12 +22,112 @@ import (
"github.com/stellar/wallet-backend/internal/db/dbtest"
"github.com/stellar/wallet-backend/internal/tss"
"github.com/stellar/wallet-backend/internal/tss/router"
tssservices "github.com/stellar/wallet-backend/internal/tss/services"
"github.com/stellar/wallet-backend/internal/tss/store"
"github.com/stellar/wallet-backend/internal/tss/utils"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
)

func TestBuildTransactions(t *testing.T) {
dbt := dbtest.Open(t)
defer dbt.Close()

dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN)
require.NoError(t, err)
defer dbConnectionPool.Close()
store, _ := store.NewStore(dbConnectionPool)
mockRouter := router.MockRouter{}
mockAppTracker := apptracker.MockAppTracker{}
mockTxService := tssservices.TransactionServiceMock{}

handler := &TSSHandler{
Router: &mockRouter,
Store: store,
AppTracker: &mockAppTracker,
NetworkPassphrase: "testnet passphrase",
TransactionService: &mockTxService,
}

srcAccount := keypair.MustRandom().Address()
p := txnbuild.Payment{
Destination: keypair.MustRandom().Address(),
Amount: "10",
Asset: txnbuild.NativeAsset{},
SourceAccount: srcAccount,
}
op, _ := p.BuildXDR()

var buf strings.Builder
enc := xdr3.NewEncoder(&buf)
_ = op.EncodeTo(enc)

opXDR := buf.String()
opXDRBase64 := base64.StdEncoding.EncodeToString([]byte(opXDR))

const endpoint = "/tss/transactions"

t.Run("tx_signing_fails", func(t *testing.T) {
reqBody := fmt.Sprintf(`{
"transactions": [{"operations": [%q], "timeout": 100}]
}`, opXDRBase64)
rw := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodPost, endpoint, strings.NewReader(reqBody))

expectedOps, _ := utils.BuildOperations([]string{opXDRBase64})

err := errors.New("unable to find channel account")
mockTxService.
On("BuildAndSignTransactionWithChannelAccount", context.Background(), expectedOps, int64(100)).
Return(nil, err).
Once()

mockAppTracker.
On("CaptureException", err).
Return().
Once()

http.HandlerFunc(handler.BuildTransactions).ServeHTTP(rw, req)
resp := rw.Result()
respBody, err := io.ReadAll(resp.Body)
require.NoError(t, err)

expectedRespBody := `{"error": "An error occurred while processing this request."}`
assert.Equal(t, http.StatusInternalServerError, resp.StatusCode)
assert.JSONEq(t, expectedRespBody, string(respBody))
})

t.Run("happy_path", func(t *testing.T) {
reqBody := fmt.Sprintf(`{
"transactions": [{"operations": [%q], "timeout": 100}]
}`, opXDRBase64)
rw := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodPost, endpoint, strings.NewReader(reqBody))

expectedOps, _ := utils.BuildOperations([]string{opXDRBase64})
tx := utils.BuildTestTransaction()

mockTxService.
On("BuildAndSignTransactionWithChannelAccount", context.Background(), expectedOps, int64(100)).
Return(tx, nil).
Once()

http.HandlerFunc(handler.BuildTransactions).ServeHTTP(rw, req)
resp := rw.Result()
respBody, err := io.ReadAll(resp.Body)
require.NoError(t, err)

assert.Equal(t, http.StatusOK, resp.StatusCode)

var buildTxResp BuildTransactionsResponse
_ = json.Unmarshal(respBody, &buildTxResp)
expectedTxXDR, _ := tx.Base64()
assert.Equal(t, expectedTxXDR, buildTxResp.TransactionXDRs[0])
})

}

func TestSubmitTransactions(t *testing.T) {
dbt := dbtest.Open(t)
defer dbt.Close()
Expand All @@ -37,12 +138,14 @@ func TestSubmitTransactions(t *testing.T) {
store, _ := store.NewStore(dbConnectionPool)
mockRouter := router.MockRouter{}
mockAppTracker := apptracker.MockAppTracker{}
txServiceMock := tssservices.TransactionServiceMock{}

handler := &TSSHandler{
Router: &mockRouter,
Store: store,
AppTracker: &mockAppTracker,
NetworkPassphrase: "testnet passphrase",
Router: &mockRouter,
Store: store,
AppTracker: &mockAppTracker,
NetworkPassphrase: "testnet passphrase",
TransactionService: &txServiceMock,
}

const endpoint = "/tss/transactions"
Expand All @@ -53,7 +156,6 @@ func TestSubmitTransactions(t *testing.T) {
req := httptest.NewRequest(http.MethodPost, endpoint, strings.NewReader(reqBody))

http.HandlerFunc(handler.SubmitTransactions).ServeHTTP(rw, req)

resp := rw.Result()
respBody, err := io.ReadAll(resp.Body)
require.NoError(t, err)
Expand All @@ -71,9 +173,9 @@ func TestSubmitTransactions(t *testing.T) {
assert.JSONEq(t, expectedRespBody, string(respBody))

reqBody = fmt.Sprintf(`{
"webhook": "localhost:8080",
"transactions": [{"operations": [%q]}]
}`, "ABCD")
"webhook": "localhost:8080",
"transactions": [%q]
}`, "ABCD")
rw = httptest.NewRecorder()
req = httptest.NewRequest(http.MethodPost, endpoint, strings.NewReader(reqBody))

Expand All @@ -83,32 +185,19 @@ func TestSubmitTransactions(t *testing.T) {
respBody, err = io.ReadAll(resp.Body)
require.NoError(t, err)

expectedRespBody = `{"error": "bad operation xdr"}`
expectedRespBody = `{"error": "bad transaction xdr"}`
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
assert.JSONEq(t, expectedRespBody, string(respBody))

})

t.Run("happy_path", func(t *testing.T) {
srcAccount := keypair.MustRandom().Address()
p := txnbuild.Payment{
Destination: keypair.MustRandom().Address(),
Amount: "10",
Asset: txnbuild.NativeAsset{},
SourceAccount: srcAccount,
}
op, _ := p.BuildXDR()

var buf strings.Builder
enc := xdr3.NewEncoder(&buf)
_ = op.EncodeTo(enc)

opXDR := buf.String()
opXDRBase64 := base64.StdEncoding.EncodeToString([]byte(opXDR))
tx := utils.BuildTestTransaction()
txXDR, _ := tx.Base64()
reqBody := fmt.Sprintf(`{
"webhook": "localhost:8080",
"transactions": [{"operations": [%q]}]
}`, opXDRBase64)
"transactions": [%q]
}`, txXDR)

rw := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodPost, endpoint, strings.NewReader(reqBody))
Expand Down Expand Up @@ -144,12 +233,14 @@ func TestGetTransaction(t *testing.T) {
store, _ := store.NewStore(dbConnectionPool)
mockRouter := router.MockRouter{}
mockAppTracker := apptracker.MockAppTracker{}
txServiceMock := tssservices.TransactionServiceMock{}

handler := &TSSHandler{
Router: &mockRouter,
Store: store,
AppTracker: &mockAppTracker,
NetworkPassphrase: "testnet passphrase",
Router: &mockRouter,
Store: store,
AppTracker: &mockAppTracker,
NetworkPassphrase: "testnet passphrase",
TransactionService: &txServiceMock,
}

endpoint := "/tss/transactions"
Expand Down
3 changes: 3 additions & 0 deletions internal/serve/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ type handlerDeps struct {
TSSRouter tssrouter.Router
PoolPopulator tssservices.PoolPopulator
TSSStore tssstore.Store
TSSTransactionService tssservices.TransactionService
// Error Tracker
AppTracker apptracker.AppTracker
}
Expand Down Expand Up @@ -284,6 +285,7 @@ func initHandlerDeps(cfg Configs) (handlerDeps, error) {
TSSRouter: router,
PoolPopulator: poolPopulator,
TSSStore: store,
TSSTransactionService: tssTxService,
}, nil
}

Expand Down Expand Up @@ -362,6 +364,7 @@ func handler(deps handlerDeps) http.Handler {
}

r.Get("/transactions/{transactionhash}", handler.GetTransaction)
r.Post("/transactions/build", handler.BuildTransactions)
r.Post("/transactions", handler.SubmitTransactions)
})
})
Expand Down
Loading