Skip to content

Commit

Permalink
Generic TSS
Browse files Browse the repository at this point in the history
  • Loading branch information
gouthamp-stellar committed Nov 26, 2024
1 parent 1ae6bf4 commit 3dd1686
Show file tree
Hide file tree
Showing 16 changed files with 1,049 additions and 282 deletions.
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 BuildTransactionResponse 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, BuildTransactionResponse{
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 BuildTransactionResponse
_ = 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

0 comments on commit 3dd1686

Please sign in to comment.