Skip to content

Commit

Permalink
httphandler: tss api endpoints (#54)
Browse files Browse the repository at this point in the history
* tables and interfaces for TSS

* TSS tables and the channel interface

* remove empty lines

* update

* adding semicolons

adding semicolons

* moving all migrations to one file

* make hash primary key instead of xdr

* missing ,

* remove the index on try_transaction_xdr and add column/index on try_transaction_hash

...also remove outgoing/incoming_status and have one status column

* Squashed commit of the following:

commit fb807aa
Author: gouthamp-stellar <[email protected]>
Date:   Tue Sep 3 10:28:49 2024 -0700

    remove the index on try_transaction_xdr and add column/index on try_transaction_hash

    ...also remove outgoing/incoming_status and have one status column

commit 6fc0dc2
Author: gouthamp-stellar <[email protected]>
Date:   Tue Sep 3 08:58:46 2024 -0700

    missing ,

commit a9cf4e3
Author: gouthamp-stellar <[email protected]>
Date:   Tue Sep 3 01:55:23 2024 -0700

    make hash primary key instead of xdr

commit c0f9d32
Author: gouthamp-stellar <[email protected]>
Date:   Fri Aug 30 15:18:27 2024 -0700

    moving all migrations to one file

commit 2de9898
Author: gouthamp-stellar <[email protected]>
Date:   Fri Aug 30 15:16:53 2024 -0700

    adding semicolons

    adding semicolons

commit 373c71a
Author: gouthamp-stellar <[email protected]>
Date:   Fri Aug 30 15:12:24 2024 -0700

    update

commit 3f9f9f0
Author: gouthamp-stellar <[email protected]>
Date:   Fri Aug 30 15:12:00 2024 -0700

    remove empty lines

commit 9920f48
Author: gouthamp-stellar <[email protected]>
Date:   Fri Aug 30 15:06:40 2024 -0700

    TSS tables and the channel interface

commit a58d519
Author: gouthamp-stellar <[email protected]>
Date:   Fri Aug 30 10:55:22 2024 -0700

    tables and interfaces for TSS

* commit #2

* changing from RpcIngestTxResponse ->  RpcGetIngestTxResponse

* latest changes

* add tests for ValidateOptions

* changes based on comments

* latest changes based on changes to interface

* string -> RPCTXStatus

* adding a transaction_builder.go which takes a list of operation xdrs and builds a transaction out of it

* moving transaction_service to the utils dir

* upper case Channel methods

* latest changes

* p.txService.NetworkPassPhrase()

* last commit before writing unit tests

* Making the transaction service more injectible and adding fields to the Payload

* typo

* typo

* Update 2024-08-28.0-tss_transactions.sql

* lint errors

* go mod tidy

* test cases + other changes

* remoce unused mocks

* error handler service returns errorHandlerService

* changes based on comments

* lint deadcode error - suppress for now

* removed deadcode

* changes after comments on transaction service pr

* TSS Error Handler Service

* removing print statements

* Update transaction_service.go

* responding to comments

* remove commented code

* tx service changes

* remove comment

* remove comment

* latest tx service changes

* adding a router + utils file

* removing println

* changed function name

* commit #1

* Code() helper function on RPCTXCode

* Code()

* Delete .env.swp

* adding a helpers file

* removing BuildTestFeeBumpTransaction

* adding to serve.go etc

* casing

* better test for Send

* resolving merge conflicts

* removing mockSleep

* delete channels/mocks.go

* sleep -> time.Sleep

* incorporating Daniel's changes + comments

* delete files

* remove unused code

* name change

* remove commented code

* moving the mocks file inside servicesmocks dir

* name changes

* refactor

* changes to serve.go

* checking error on route.Route

* check error

* merging main

* fixing parsesendresp tests

* commit changes

* merging in changes from eror_handler_service

* delete file

* commit current changes before merging branch

* rpc ingestor

* delete file

* fix lint errors

* remove dead code

* fix broken test + delete unused code

* tidy mod

* remove commented code

* delete file

* changes based on comments

* removing test case that is not relevant anymore

* changes based on prev pr comments

* remove fmt.Println

* merge latest error_handler_service branch + small changes

* %s -> %w

* U -> u

* variable for channel name

* account for NewStore returning an error

* fix build

* commit #1

* tss pool populator

* typo

* typo

* tss api endpoints

* spaces

* changing the tss api endpoint paths

* merge main

* merged latest

* typo in desc string

* millisecond wait time

* changes based on comments

* typo

* changes based on comments

* fixing migrations

* compare types directly on route

* changes based on comments

* remove unused funcs in tss_options.go
  • Loading branch information
gouthamp-stellar authored Oct 29, 2024
1 parent dd63a2b commit 8469e6f
Show file tree
Hide file tree
Showing 6 changed files with 352 additions and 2 deletions.
1 change: 0 additions & 1 deletion cmd/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@ func (c *serveCmd) Command() *cobra.Command {
utils.WebhookHandlerChannelMaxWorkersOptions(&cfg.WebhookHandlerServiceChannelMaxWorkers),
utils.WebhookHandlerChannelMaxRetriesOption(&cfg.WebhookHandlerServiceChannelMaxRetries),
utils.WebhookHandlerChannelMinWaitBtwnRetriesMSOption(&cfg.WebhookHandlerServiceChannelMinWaitBtwnRetriesMS),

{
Name: "port",
Usage: "Port to listen and serve on",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,4 @@ ALTER TABLE tss_transaction_submission_tries
DROP COLUMN result_xdr;

ALTER TABLE tss_transaction_submission_tries
RENAME COLUMN code TO status;
RENAME COLUMN code TO status;
122 changes: 122 additions & 0 deletions internal/serve/httphandler/tss_handler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
package httphandler

import (
"net/http"

"github.com/stellar/go/support/log"
"github.com/stellar/go/support/render/httpjson"
"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"
"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
}

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

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

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

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 _, transaction := range reqParams.Transactions {
tx, err := utils.BuildOriginalTransaction(transaction.Operations)
if err != nil {
httperror.BadRequest("bad operation 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
}

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

payload := tss.Payload{
TransactionHash: txHash,
TransactionXDR: txXDR,
WebhookURL: reqParams.WebhookURL,
}

payloads = append(payloads, payload)
transactionHashes = append(transactionHashes, txHash)
}
httpjson.Render(w, TransactionSubmissionResponse{
TransactionHashes: transactionHashes,
}, httpjson.JSON)

for _, payload := range payloads {
err := t.Router.Route(payload)
if err != nil {
log.Errorf("unable to route payload: %v", err)
}

}

}

type GetTransactionRequest struct {
TransactionHash string `json:"transactionhash" validate:"required"`
}

type GetTransactionResponse struct {
Hash string `json:"transactionhash"`
XDR string `json:"transactionxdr"`
Status string `json:"status"`
}

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

var reqParams GetTransactionRequest
httpErr := DecodePathAndValidate(ctx, r, &reqParams, t.AppTracker)
if httpErr != nil {
httpErr.Render(w)
return
}
tx, err := t.Store.GetTransaction(ctx, reqParams.TransactionHash)
if err != nil {
httperror.InternalServerError(ctx, "unable to get transaction "+reqParams.TransactionHash, err, nil, t.AppTracker).Render(w)
return
}

if tx == (store.Transaction{}) {
httperror.NotFound.Render(w)
}

httpjson.Render(w, GetTransactionResponse{
Hash: tx.Hash,
XDR: tx.XDR,
Status: tx.Status,
}, httpjson.JSON)
}
212 changes: 212 additions & 0 deletions internal/serve/httphandler/tss_handler_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
package httphandler

import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"net/http"
"net/http/httptest"
"path"
"strings"
"testing"

"github.com/go-chi/chi"
xdr3 "github.com/stellar/go-xdr/xdr3"
"github.com/stellar/go/keypair"
"github.com/stellar/go/txnbuild"
"github.com/stellar/wallet-backend/internal/apptracker"
"github.com/stellar/wallet-backend/internal/db"
"github.com/stellar/wallet-backend/internal/db/dbtest"
"github.com/stellar/wallet-backend/internal/tss"
"github.com/stellar/wallet-backend/internal/tss/router"
"github.com/stellar/wallet-backend/internal/tss/store"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
)

func TestSubmitTransactions(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{}

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

const endpoint = "/tss/transactions"

t.Run("invalid_request_bodies", func(t *testing.T) {
reqBody := `{}`
rw := httptest.NewRecorder()
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)

expectedRespBody := `
{
"error": "Validation error.",
"extras": {
"transactions": "This field is required",
"webhookURL": "This field is required"
}
}`

assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
assert.JSONEq(t, expectedRespBody, string(respBody))

reqBody = fmt.Sprintf(`{
"webhook": "localhost:8080",
"transactions": [{"operations": [%q]}]
}`, "ABCD")
rw = httptest.NewRecorder()
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)

expectedRespBody = `{"error": "bad operation 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))
reqBody := fmt.Sprintf(`{
"webhook": "localhost:8080",
"transactions": [{"operations": [%q]}]
}`, opXDRBase64)

rw := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodPost, endpoint, strings.NewReader(reqBody))

mockRouter.
On("Route", mock.Anything).
Return(nil).
Once()

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

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

var txSubmissionResp TransactionSubmissionResponse
_ = json.Unmarshal(respBody, &txSubmissionResp)

assert.Equal(t, 1, len(txSubmissionResp.TransactionHashes))

mockRouter.AssertNumberOfCalls(t, "Route", 1)
})
}

func TestGetTransaction(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{}

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

endpoint := "/tss/transactions"

r := chi.NewRouter()
r.Route(endpoint, func(r chi.Router) {
r.Get("/{transactionhash}", handler.GetTransaction)
})

clearTransactions := func(ctx context.Context) {
_, err = dbConnectionPool.ExecContext(ctx, "TRUNCATE tss_transactions")
require.NoError(t, err)
}

t.Run("returns_transaction", func(t *testing.T) {
txHash := "hash"
ctx := context.Background()
_ = store.UpsertTransaction(ctx, "localhost:8080/webhook", txHash, "xdr", tss.RPCTXStatus{OtherStatus: tss.NewStatus})
req, err := http.NewRequest(http.MethodGet, path.Join(endpoint, txHash), nil)
require.NoError(t, err)

// Serve request
rw := httptest.NewRecorder()
r.ServeHTTP(rw, req)
resp := rw.Result()
respBody, err := io.ReadAll(resp.Body)
require.NoError(t, err)
var getTxResp GetTransactionResponse
_ = json.Unmarshal(respBody, &getTxResp)

assert.Equal(t, http.StatusOK, resp.StatusCode)
assert.Equal(t, "hash", getTxResp.Hash)
assert.Equal(t, "xdr", getTxResp.XDR)
assert.Equal(t, "NEW", getTxResp.Status)

clearTransactions(ctx)
})

t.Run("return_empty_transaction", func(t *testing.T) {
txHash := "hash"
req, err := http.NewRequest(http.MethodGet, path.Join(endpoint, txHash), nil)
require.NoError(t, err)

// Serve request
rw := httptest.NewRecorder()
r.ServeHTTP(rw, req)
resp := rw.Result()
respBody, err := io.ReadAll(resp.Body)
require.NoError(t, err)
var getTxResp GetTransactionResponse
_ = json.Unmarshal(respBody, &getTxResp)

assert.Equal(t, http.StatusNotFound, resp.StatusCode)
assert.Empty(t, getTxResp.Hash)
assert.Empty(t, getTxResp.XDR)
assert.Empty(t, getTxResp.Status)

})

}
Loading

0 comments on commit 8469e6f

Please sign in to comment.