diff --git a/internal/serve/httphandler/tss_handler.go b/internal/serve/httphandler/tss_handler.go index 3925a4d..f637911 100644 --- a/internal/serve/httphandler/tss_handler.go +++ b/internal/serve/httphandler/tss_handler.go @@ -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) @@ -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 { diff --git a/internal/serve/httphandler/tss_handler_test.go b/internal/serve/httphandler/tss_handler_test.go index d982a6d..92d5587 100644 --- a/internal/serve/httphandler/tss_handler_test.go +++ b/internal/serve/httphandler/tss_handler_test.go @@ -4,6 +4,7 @@ import ( "context" "encoding/base64" "encoding/json" + "errors" "fmt" "io" "net/http" @@ -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() @@ -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" @@ -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) @@ -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)) @@ -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)) @@ -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" diff --git a/internal/serve/serve.go b/internal/serve/serve.go index 5c4eacd..242adb6 100644 --- a/internal/serve/serve.go +++ b/internal/serve/serve.go @@ -104,6 +104,7 @@ type handlerDeps struct { TSSRouter tssrouter.Router PoolPopulator tssservices.PoolPopulator TSSStore tssstore.Store + TSSTransactionService tssservices.TransactionService // Error Tracker AppTracker apptracker.AppTracker } @@ -284,6 +285,7 @@ func initHandlerDeps(cfg Configs) (handlerDeps, error) { TSSRouter: router, PoolPopulator: poolPopulator, TSSStore: store, + TSSTransactionService: tssTxService, }, nil } @@ -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) }) }) diff --git a/internal/tss/services/mocks.go b/internal/tss/services/mocks.go index 3edcb26..0a49e82 100644 --- a/internal/tss/services/mocks.go +++ b/internal/tss/services/mocks.go @@ -20,6 +20,23 @@ func (t *TransactionServiceMock) NetworkPassphrase() string { return args.String(0) } +func (t *TransactionServiceMock) BuildAndSignTransactionWithChannelAccount(ctx context.Context, operations []txnbuild.Operation, timeoutInSecs int64) (*txnbuild.Transaction, error) { + args := t.Called(ctx, operations, timeoutInSecs) + if result := args.Get(0); result != nil { + return result.(*txnbuild.Transaction), args.Error(1) + } + return nil, args.Error(1) +} + +func (t *TransactionServiceMock) BuildFeeBumpTransaction(ctx context.Context, tx *txnbuild.Transaction) (*txnbuild.FeeBumpTransaction, error) { + args := t.Called(ctx, tx) + if result := args.Get(0); result != nil { + return result.(*txnbuild.FeeBumpTransaction), args.Error(1) + } + return nil, args.Error(1) +} + +/* func (t *TransactionServiceMock) SignAndBuildNewFeeBumpTransaction(ctx context.Context, origTxXdr string) (*txnbuild.FeeBumpTransaction, error) { args := t.Called(ctx, origTxXdr) if result := args.Get(0); result != nil { @@ -28,7 +45,9 @@ func (t *TransactionServiceMock) SignAndBuildNewFeeBumpTransaction(ctx context.C return nil, args.Error(1) } +*/ +/* func (t *TransactionServiceMock) SendTransaction(transactionXdr string) (tss.RPCSendTxResponse, error) { args := t.Called(transactionXdr) return args.Get(0).(tss.RPCSendTxResponse), args.Error(1) @@ -38,6 +57,7 @@ func (t *TransactionServiceMock) GetTransaction(transactionHash string) (tss.RPC args := t.Called(transactionHash) return args.Get(0).(tss.RPCGetIngestTxResponse), args.Error(1) } +*/ type TransactionManagerMock struct { mock.Mock diff --git a/internal/tss/services/pool_populator.go b/internal/tss/services/pool_populator.go index 5a78d44..efba218 100644 --- a/internal/tss/services/pool_populator.go +++ b/internal/tss/services/pool_populator.go @@ -4,10 +4,8 @@ import ( "context" "fmt" "slices" - "time" "github.com/stellar/go/support/log" - "github.com/stellar/go/txnbuild" "github.com/stellar/go/xdr" "github.com/stellar/wallet-backend/internal/entities" "github.com/stellar/wallet-backend/internal/services" @@ -85,44 +83,8 @@ func (p *poolPopulator) routeNewTransactions(ctx context.Context) error { if err != nil { return fmt.Errorf("getting latest try for transaction: %w", err) } - if try == (store.Try{}) { - // there is no try for this transactionm - route to RPC caller channel + if try == (store.Try{}) || try.Code == int32(tss.RPCFailCode) || try.Code == int32(tss.NewCode) { payload.RpcSubmitTxResponse.Status = tss.RPCTXStatus{OtherStatus: tss.NewStatus} - } else { - /* - if there is a try for this transaction, check to see if it is - submitted to RPC first. If status is NOT_FOUND, make sure - that the latest try for this transaction is past it's timebounds - before trying to re-submit the transaction. If the status is either - SUCCESS or FAILED, build a payload that will be routed to the Webhook - channel directly - */ - getTransactionResult, err := p.RPCService.GetTransaction(try.Hash) - if err != nil { - return fmt.Errorf("getting transaction: %w", err) - } - if getTransactionResult.Status == entities.NotFoundStatus { - genericTx, err := txnbuild.TransactionFromXDR(try.XDR) - if err != nil { - return fmt.Errorf("unmarshaling tx from xdr string: %w", err) - } - feeBumpTx, unpackable := genericTx.FeeBump() - if !unpackable { - return fmt.Errorf("fee bump transaction cannot be unpacked: %w", err) - } - timeBounds := feeBumpTx.InnerTransaction().ToXDR().Preconditions().TimeBounds - if time.Now().Before(time.Unix(int64(timeBounds.MaxTime), 0)) { - continue - } - // route to the RPC Caller channel - payload.RpcSubmitTxResponse.Status = tss.RPCTXStatus{OtherStatus: tss.NewStatus} - } else { - getIngestTxResponse, err := tss.ParseToRPCGetIngestTxResponse(getTransactionResult, err) - if err != nil { - return fmt.Errorf("parsing rpc reponse: %w", err) - } - payload.RpcGetIngestTxResponse = getIngestTxResponse - } } err = p.Router.Route(payload) if err != nil { @@ -156,20 +118,8 @@ func (p *poolPopulator) routeErrorTransactions(ctx context.Context) error { Code: tss.RPCTXCode{TxResultCode: xdr.TransactionResultCode(try.Code)}, ErrorResultXDR: try.ResultXDR, } - } else if try.Code == int32(tss.RPCFailCode) || try.Code == int32(tss.UnmarshalBinaryCode) { - // check for timebounds first and route iff out of timebounds route to errorchannel - genericTx, err := txnbuild.TransactionFromXDR(try.XDR) - if err != nil { - return fmt.Errorf("unmarshaling tx from xdr string: %w", err) - } - feeBumpTx, unpackable := genericTx.FeeBump() - if !unpackable { - return fmt.Errorf("fee bump transaction cannot be unpacked: %w", err) - } - timeBounds := feeBumpTx.InnerTransaction().ToXDR().Preconditions().TimeBounds - if time.Now().Before(time.Unix(int64(timeBounds.MaxTime), 0)) { - continue - } + } else if try.Code == int32(tss.RPCFailCode) || try.Code == int32(tss.NewCode) { + // route to the error jitter channel payload.RpcSubmitTxResponse = tss.RPCSendTxResponse{ TransactionHash: try.Hash, TransactionXDR: try.XDR, diff --git a/internal/tss/services/pool_populator_test.go b/internal/tss/services/pool_populator_test.go index 4304dc9..6ba2c2f 100644 --- a/internal/tss/services/pool_populator_test.go +++ b/internal/tss/services/pool_populator_test.go @@ -12,7 +12,6 @@ import ( "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" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -50,25 +49,13 @@ func TestRouteNewTransactions(t *testing.T) { _ = store.UpsertTransaction(context.Background(), "localhost:8000/webhook", "hash", "xdr", tss.RPCTXStatus{OtherStatus: tss.NewStatus}) _ = store.UpsertTry(context.Background(), "hash", "feebumphash", "feebumpxdr", tss.RPCTXStatus{OtherStatus: tss.NewStatus}, tss.RPCTXCode{OtherCodes: tss.NewCode}, "ABCD") - rpcGetTransacrionResp := entities.RPCGetTransactionResult{ - Status: entities.ErrorStatus, - EnvelopeXDR: "envelopexdr", - ResultXDR: "AAAAAAARFy8AAAAAAAAAAQAAAAAAAAAYAAAAAMu8SHUN67hTUJOz3q+IrH9M/4dCVXaljeK6x1Ss20YWAAAAAA==", - CreatedAt: "1234", - } - - mockRPCSerive. - On("GetTransaction", "feebumphash"). - Return(rpcGetTransacrionResp, nil). - Once() - - getIngestTxResp, _ := tss.ParseToRPCGetIngestTxResponse(rpcGetTransacrionResp, nil) expectedPayload := tss.Payload{ - TransactionHash: "hash", - TransactionXDR: "xdr", - WebhookURL: "localhost:8000/webhook", - RpcGetIngestTxResponse: getIngestTxResp, + TransactionHash: "hash", + TransactionXDR: "xdr", + WebhookURL: "localhost:8000/webhook", + RpcSubmitTxResponse: tss.RPCSendTxResponse{Status: tss.RPCTXStatus{OtherStatus: tss.NewStatus}}, } + mockRouter. On("Route", expectedPayload). Return(nil). @@ -77,26 +64,6 @@ func TestRouteNewTransactions(t *testing.T) { err := populator.routeNewTransactions(context.Background()) assert.Empty(t, err) }) - - t.Run("tx_not_found_timebounds_not_exceeded", func(t *testing.T) { - feeBumpTx := utils.BuildTestFeeBumpTransaction() - txXDRStr, _ := feeBumpTx.Base64() - _ = store.UpsertTransaction(context.Background(), "localhost:8000/webhook", "hash", "xdr", tss.RPCTXStatus{OtherStatus: tss.NewStatus}) - _ = store.UpsertTry(context.Background(), "hash", "feebumphash", txXDRStr, tss.RPCTXStatus{OtherStatus: tss.NewStatus}, tss.RPCTXCode{OtherCodes: tss.NewCode}, "ABCD") - - rpcGetTransacrionResp := entities.RPCGetTransactionResult{ - Status: entities.NotFoundStatus, - EnvelopeXDR: "envelopexdr", - } - - mockRPCSerive. - On("GetTransaction", "feebumphash"). - Return(rpcGetTransacrionResp, nil). - Once() - - err := populator.routeNewTransactions(context.Background()) - assert.Empty(t, err) - }) } func TestRouteErrorTransactions(t *testing.T) { @@ -136,14 +103,30 @@ func TestRouteErrorTransactions(t *testing.T) { err := populator.routeErrorTransactions(context.Background()) assert.Empty(t, err) }) - t.Run("tx_timebounds_not_exceeded", func(t *testing.T) { - feeBumpTx := utils.BuildTestFeeBumpTransaction() - txXDRStr, _ := feeBumpTx.Base64() + + t.Run("latest_try_rpc_call_failed", func(t *testing.T) { _ = store.UpsertTransaction(context.Background(), "localhost:8000/webhook", "hash", "xdr", tss.RPCTXStatus{RPCStatus: entities.ErrorStatus}) - _ = store.UpsertTry(context.Background(), "hash", "feebumphash", txXDRStr, tss.RPCTXStatus{RPCStatus: entities.ErrorStatus}, tss.RPCTXCode{OtherCodes: tss.RPCFailCode}, "ABCD") + _ = store.UpsertTry(context.Background(), "hash", "feebumphash", "feebumpxdr", tss.RPCTXStatus{RPCStatus: entities.ErrorStatus}, tss.RPCTXCode{OtherCodes: tss.RPCFailCode}, "ABCD") + + expectedPayload := tss.Payload{ + TransactionHash: "hash", + TransactionXDR: "xdr", + WebhookURL: "localhost:8000/webhook", + RpcSubmitTxResponse: tss.RPCSendTxResponse{ + TransactionHash: "feebumphash", + TransactionXDR: "feebumpxdr", + Status: tss.RPCTXStatus{RPCStatus: entities.TryAgainLaterStatus}, + }, + } + + mockRouter. + On("Route", expectedPayload). + Return(nil). + Once() err := populator.routeErrorTransactions(context.Background()) assert.Empty(t, err) + }) } diff --git a/internal/tss/services/transaction_manager.go b/internal/tss/services/transaction_manager.go index 0a88573..6e180de 100644 --- a/internal/tss/services/transaction_manager.go +++ b/internal/tss/services/transaction_manager.go @@ -4,8 +4,10 @@ import ( "context" "fmt" + "github.com/stellar/go/txnbuild" "github.com/stellar/wallet-backend/internal/services" "github.com/stellar/wallet-backend/internal/tss" + "github.com/stellar/wallet-backend/internal/tss/errors" "github.com/stellar/wallet-backend/internal/tss/store" ) @@ -33,29 +35,51 @@ func NewTransactionManager(cfg TransactionManagerConfigs) *transactionManager { } } +// this function will now take in a new parameter whether to wrap this in a fee bump or not func (t *transactionManager) BuildAndSubmitTransaction(ctx context.Context, channelName string, payload tss.Payload) (tss.RPCSendTxResponse, error) { - feeBumpTx, err := t.TxService.SignAndBuildNewFeeBumpTransaction(ctx, payload.TransactionXDR) + genericTx, err := txnbuild.TransactionFromXDR(payload.TransactionXDR) if err != nil { - return tss.RPCSendTxResponse{}, fmt.Errorf("%s: Unable to sign/build transaction: %w", channelName, err) + return tss.RPCSendTxResponse{}, errors.OriginalXDRMalformed } - feeBumpTxHash, err := feeBumpTx.HashHex(t.TxService.NetworkPassphrase()) - if err != nil { - return tss.RPCSendTxResponse{}, fmt.Errorf("%s: Unable to hashhex fee bump transaction: %w", channelName, err) + tx, txEmpty := genericTx.Transaction() + if !txEmpty { + return tss.RPCSendTxResponse{}, errors.OriginalXDRMalformed } + var tryTxHash string + var tryTxXDR string + if payload.FeeBump { + feeBumpTx, err := t.TxService.BuildFeeBumpTransaction(ctx, tx) + if err != nil { + return tss.RPCSendTxResponse{}, fmt.Errorf("%s: Unable to build fee bump transaction: %w", channelName, err) + } + tryTxHash, err = feeBumpTx.HashHex(t.TxService.NetworkPassphrase()) + if err != nil { + return tss.RPCSendTxResponse{}, fmt.Errorf("%s: Unable to hashhex fee bump transaction: %w", channelName, err) + } + tryTxXDR, err = feeBumpTx.Base64() + if err != nil { + return tss.RPCSendTxResponse{}, fmt.Errorf("%s: Unable to base64 fee bump transaction: %w", channelName, err) + } - feeBumpTxXDR, err := feeBumpTx.Base64() - if err != nil { - return tss.RPCSendTxResponse{}, fmt.Errorf("%s: Unable to base64 fee bump transaction: %w", channelName, err) + } else { + tryTxHash, err = tx.HashHex(t.TxService.NetworkPassphrase()) + if err != nil { + return tss.RPCSendTxResponse{}, fmt.Errorf("%s: Unable to hashhex transaction: %w", channelName, err) + } + tryTxXDR, err = tx.Base64() + if err != nil { + return tss.RPCSendTxResponse{}, fmt.Errorf("%s: Unable to base64 transaction: %w", channelName, err) + } } - err = t.Store.UpsertTry(ctx, payload.TransactionHash, feeBumpTxHash, feeBumpTxXDR, tss.RPCTXStatus{OtherStatus: tss.NewStatus}, tss.RPCTXCode{OtherCodes: tss.NewCode}, "") + err = t.Store.UpsertTry(ctx, payload.TransactionHash, tryTxHash, tryTxXDR, tss.RPCTXStatus{OtherStatus: tss.NewStatus}, tss.RPCTXCode{OtherCodes: tss.NewCode}, "") if err != nil { return tss.RPCSendTxResponse{}, fmt.Errorf("%s: Unable to upsert try in tries table: %w", channelName, err) } - rpcResp, rpcErr := t.RPCService.SendTransaction(feeBumpTxXDR) - rpcSendResp, parseErr := tss.ParseToRPCSendTxResponse(feeBumpTxHash, rpcResp, rpcErr) + rpcResp, rpcErr := t.RPCService.SendTransaction(tryTxXDR) + rpcSendResp, parseErr := tss.ParseToRPCSendTxResponse(tryTxHash, rpcResp, rpcErr) - err = t.Store.UpsertTry(ctx, payload.TransactionHash, feeBumpTxHash, feeBumpTxXDR, rpcSendResp.Status, rpcSendResp.Code, rpcResp.ErrorResultXDR) + err = t.Store.UpsertTry(ctx, payload.TransactionHash, tryTxHash, tryTxXDR, rpcSendResp.Status, rpcSendResp.Code, rpcResp.ErrorResultXDR) if err != nil { return tss.RPCSendTxResponse{}, fmt.Errorf("%s: Unable to upsert try in tries table: %s", channelName, err.Error()) } diff --git a/internal/tss/services/transaction_manager_test.go b/internal/tss/services/transaction_manager_test.go index 0071200..c8771a8 100644 --- a/internal/tss/services/transaction_manager_test.go +++ b/internal/tss/services/transaction_manager_test.go @@ -33,25 +33,29 @@ func TestBuildAndSubmitTransaction(t *testing.T) { Store: store, }) networkPass := "passphrase" + tx := utils.BuildTestTransaction() + txHash, _ := tx.HashHex(networkPass) + txXDR, _ := tx.Base64() feeBumpTx := utils.BuildTestFeeBumpTransaction() feeBumpTxXDR, _ := feeBumpTx.Base64() feeBumpTxHash, _ := feeBumpTx.HashHex(networkPass) payload := tss.Payload{} payload.WebhookURL = "www.stellar.com" - payload.TransactionHash = "hash" - payload.TransactionXDR = "xdr" + payload.TransactionHash = txHash + payload.TransactionXDR = txXDR - t.Run("fail_on_tx_build_and_sign", func(t *testing.T) { + t.Run("fail_on_building_feebump_tx", func(t *testing.T) { _ = store.UpsertTransaction(context.Background(), payload.WebhookURL, payload.TransactionHash, payload.TransactionXDR, tss.RPCTXStatus{OtherStatus: tss.NewStatus}) txServiceMock. - On("SignAndBuildNewFeeBumpTransaction", context.Background(), payload.TransactionXDR). + On("BuildFeeBumpTransaction", context.Background(), tx). Return(nil, errors.New("signing failed")). Once() + payload.FeeBump = true txSendResp, err := txManager.BuildAndSubmitTransaction(context.Background(), "channel", payload) assert.Equal(t, tss.RPCSendTxResponse{}, txSendResp) - assert.Equal(t, "channel: Unable to sign/build transaction: signing failed", err.Error()) + assert.Equal(t, "channel: Unable to build fee bump transaction: signing failed", err.Error()) tx, _ := store.GetTransaction(context.Background(), payload.TransactionHash) assert.Equal(t, string(tss.NewStatus), tx.Status) @@ -62,7 +66,7 @@ func TestBuildAndSubmitTransaction(t *testing.T) { sendResp := entities.RPCSendTransactionResult{Status: entities.ErrorStatus} txServiceMock. - On("SignAndBuildNewFeeBumpTransaction", context.Background(), payload.TransactionXDR). + On("BuildFeeBumpTransaction", context.Background(), tx). Return(feeBumpTx, nil). Once(). On("NetworkPassphrase"). @@ -72,6 +76,7 @@ func TestBuildAndSubmitTransaction(t *testing.T) { On("SendTransaction", feeBumpTxXDR). Return(sendResp, errors.New("RPC down")). Once() + payload.FeeBump = true txSendResp, err := txManager.BuildAndSubmitTransaction(context.Background(), "channel", payload) @@ -95,7 +100,7 @@ func TestBuildAndSubmitTransaction(t *testing.T) { } txServiceMock. - On("SignAndBuildNewFeeBumpTransaction", context.Background(), payload.TransactionXDR). + On("BuildFeeBumpTransaction", context.Background(), tx). Return(feeBumpTx, nil). Once(). On("NetworkPassphrase"). @@ -105,6 +110,7 @@ func TestBuildAndSubmitTransaction(t *testing.T) { On("SendTransaction", feeBumpTxXDR). Return(sendResp, nil). Once() + payload.FeeBump = true txSendResp, err := txManager.BuildAndSubmitTransaction(context.Background(), "channel", payload) @@ -128,7 +134,7 @@ func TestBuildAndSubmitTransaction(t *testing.T) { } txServiceMock. - On("SignAndBuildNewFeeBumpTransaction", context.Background(), payload.TransactionXDR). + On("BuildFeeBumpTransaction", context.Background(), tx). Return(feeBumpTx, nil). Once(). On("NetworkPassphrase"). @@ -138,6 +144,7 @@ func TestBuildAndSubmitTransaction(t *testing.T) { On("SendTransaction", feeBumpTxXDR). Return(sendResp, nil). Once() + payload.FeeBump = true txSendResp, err := txManager.BuildAndSubmitTransaction(context.Background(), "channel", payload) @@ -152,7 +159,6 @@ func TestBuildAndSubmitTransaction(t *testing.T) { assert.Equal(t, string(entities.ErrorStatus), try.Status) assert.Equal(t, int32(tss.UnmarshalBinaryCode), try.Code) }) - t.Run("rpc_returns_response", func(t *testing.T) { _ = store.UpsertTransaction(context.Background(), payload.WebhookURL, payload.TransactionHash, payload.TransactionXDR, tss.RPCTXStatus{OtherStatus: tss.NewStatus}) sendResp := entities.RPCSendTransactionResult{ @@ -161,7 +167,7 @@ func TestBuildAndSubmitTransaction(t *testing.T) { } txServiceMock. - On("SignAndBuildNewFeeBumpTransaction", context.Background(), payload.TransactionXDR). + On("BuildFeeBumpTransaction", context.Background(), tx). Return(feeBumpTx, nil). Once(). On("NetworkPassphrase"). @@ -171,6 +177,7 @@ func TestBuildAndSubmitTransaction(t *testing.T) { On("SendTransaction", feeBumpTxXDR). Return(sendResp, nil). Once() + payload.FeeBump = true txSendResp, err := txManager.BuildAndSubmitTransaction(context.Background(), "channel", payload) @@ -185,4 +192,38 @@ func TestBuildAndSubmitTransaction(t *testing.T) { assert.Equal(t, string(entities.ErrorStatus), try.Status) assert.Equal(t, int32(xdr.TransactionResultCodeTxTooLate), try.Code) }) + t.Run("feebump_is_false", func(t *testing.T) { + _ = store.UpsertTransaction(context.Background(), payload.WebhookURL, payload.TransactionHash, payload.TransactionXDR, tss.RPCTXStatus{OtherStatus: tss.NewStatus}) + sendResp := entities.RPCSendTransactionResult{ + Status: entities.ErrorStatus, + ErrorResultXDR: "AAAAAAAAAMj////9AAAAAA==", + } + txServiceMock. + On("NetworkPassphrase"). + Return(networkPass). + Once() + rpcServiceMock. + On("SendTransaction", txXDR). + Return(sendResp, nil). + Once() + rpcServiceMock. + On("SendTransaction", feeBumpTxXDR). + Return(sendResp, nil). + Once() + payload.FeeBump = false + + txSendResp, err := txManager.BuildAndSubmitTransaction(context.Background(), "channel", payload) + + assert.Equal(t, entities.ErrorStatus, txSendResp.Status.RPCStatus) + assert.Equal(t, xdr.TransactionResultCodeTxTooLate, txSendResp.Code.TxResultCode) + assert.Empty(t, err) + + tx, _ := store.GetTransaction(context.Background(), payload.TransactionHash) + assert.Equal(t, string(entities.ErrorStatus), tx.Status) + + try, _ := store.GetTry(context.Background(), txHash) + assert.Equal(t, string(entities.ErrorStatus), try.Status) + assert.Equal(t, int32(xdr.TransactionResultCodeTxTooLate), try.Code) + + }) } diff --git a/internal/tss/services/transaction_service.go b/internal/tss/services/transaction_service.go index f3afd90..d998627 100644 --- a/internal/tss/services/transaction_service.go +++ b/internal/tss/services/transaction_service.go @@ -5,15 +5,14 @@ import ( "fmt" "github.com/stellar/go/clients/horizonclient" - "github.com/stellar/go/support/log" "github.com/stellar/go/txnbuild" "github.com/stellar/wallet-backend/internal/signing" - tsserror "github.com/stellar/wallet-backend/internal/tss/errors" ) type TransactionService interface { NetworkPassphrase() string - SignAndBuildNewFeeBumpTransaction(ctx context.Context, origTxXdr string) (*txnbuild.FeeBumpTransaction, error) + BuildAndSignTransactionWithChannelAccount(ctx context.Context, operations []txnbuild.Operation, timeoutInSecs int64) (*txnbuild.Transaction, error) + BuildFeeBumpTransaction(ctx context.Context, tx *txnbuild.Transaction) (*txnbuild.FeeBumpTransaction, error) } type transactionService struct { @@ -68,34 +67,7 @@ func (t *transactionService) NetworkPassphrase() string { return t.DistributionAccountSignatureClient.NetworkPassphrase() } -func buildPayments(srcAccount string, operations []txnbuild.Operation) ([]txnbuild.Operation, error) { - var payments []txnbuild.Operation - for _, op := range operations { - origPayment, ok := op.(*txnbuild.Payment) - if !ok { - return nil, fmt.Errorf("unable to convert operation to payment op") - } - payment := &txnbuild.Payment{ - SourceAccount: srcAccount, - Amount: origPayment.Amount, - Destination: origPayment.Destination, - Asset: origPayment.Asset, - } - payments = append(payments, payment) - - } - return payments, nil -} - -func (t *transactionService) SignAndBuildNewFeeBumpTransaction(ctx context.Context, origTxXdr string) (*txnbuild.FeeBumpTransaction, error) { - genericTx, err := txnbuild.TransactionFromXDR(origTxXdr) - if err != nil { - return nil, tsserror.OriginalXDRMalformed - } - originalTx, txEmpty := genericTx.Transaction() - if !txEmpty { - return nil, tsserror.OriginalXDRMalformed - } +func (t *transactionService) BuildAndSignTransactionWithChannelAccount(ctx context.Context, operations []txnbuild.Operation, timeoutInSecs int64) (*txnbuild.Transaction, error) { channelAccountPublicKey, err := t.ChannelAccountSignatureClient.GetAccountPublicKey(ctx) if err != nil { return nil, fmt.Errorf("getting channel account public key: %w", err) @@ -104,25 +76,13 @@ func (t *transactionService) SignAndBuildNewFeeBumpTransaction(ctx context.Conte if err != nil { return nil, fmt.Errorf("getting channel account details from horizon: %w", err) } - - distributionAccountPublicKey, err := t.DistributionAccountSignatureClient.GetAccountPublicKey(ctx) - if err != nil { - return nil, fmt.Errorf("getting distribution account public key: %w", err) - } - - operations, err := buildPayments(distributionAccountPublicKey, originalTx.Operations()) - if err != nil { - return nil, fmt.Errorf("building payment operations: %w", err) - } - log.Info(operations) - tx, err := txnbuild.NewTransaction( txnbuild.TransactionParams{ SourceAccount: &channelAccount, Operations: operations, BaseFee: int64(t.BaseFee), Preconditions: txnbuild.Preconditions{ - TimeBounds: txnbuild.NewTimeout(120), + TimeBounds: txnbuild.NewTimeout(timeoutInSecs), }, IncrementSequenceNum: true, }, @@ -134,12 +94,14 @@ func (t *transactionService) SignAndBuildNewFeeBumpTransaction(ctx context.Conte if err != nil { return nil, fmt.Errorf("signing transaction with channel account: %w", err) } + return tx, nil +} - tx, err = t.DistributionAccountSignatureClient.SignStellarTransaction(ctx, tx, distributionAccountPublicKey) +func (t *transactionService) BuildFeeBumpTransaction(ctx context.Context, tx *txnbuild.Transaction) (*txnbuild.FeeBumpTransaction, error) { + distributionAccountPublicKey, err := t.DistributionAccountSignatureClient.GetAccountPublicKey(ctx) if err != nil { - return nil, fmt.Errorf("signing transaction with distribution account: %w", err) + return nil, fmt.Errorf("getting distribution account public key: %w", err) } - feeBumpTx, err := txnbuild.NewFeeBumpTransaction( txnbuild.FeeBumpTransactionParams{ Inner: tx, diff --git a/internal/tss/services/transaction_service_test.go b/internal/tss/services/transaction_service_test.go index 3035932..e573f87 100644 --- a/internal/tss/services/transaction_service_test.go +++ b/internal/tss/services/transaction_service_test.go @@ -10,7 +10,6 @@ import ( "github.com/stellar/go/protocols/horizon" "github.com/stellar/go/txnbuild" "github.com/stellar/wallet-backend/internal/signing" - tsserror "github.com/stellar/wallet-backend/internal/tss/errors" "github.com/stellar/wallet-backend/internal/tss/utils" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" @@ -63,6 +62,7 @@ func TestValidateOptions(t *testing.T) { }) } +/* func TestBuildPayments(t *testing.T) { dest := "ABCD" operations := []txnbuild.Operation{ @@ -79,7 +79,204 @@ func TestBuildPayments(t *testing.T) { assert.Equal(t, dest, payments[0].(*txnbuild.Payment).Destination) assert.Equal(t, txnbuild.NativeAsset{}, payments[0].(*txnbuild.Payment).Asset) } +*/ +func TestBuildAndSignTransactionWithChannelAccount(t *testing.T) { + distributionAccountSignatureClient := signing.SignatureClientMock{} + defer distributionAccountSignatureClient.AssertExpectations(t) + channelAccountSignatureClient := signing.SignatureClientMock{} + defer channelAccountSignatureClient.AssertExpectations(t) + horizonClient := horizonclient.MockClient{} + defer horizonClient.AssertExpectations(t) + txService, _ := NewTransactionService(TransactionServiceOptions{ + DistributionAccountSignatureClient: &distributionAccountSignatureClient, + ChannelAccountSignatureClient: &channelAccountSignatureClient, + HorizonClient: &horizonClient, + BaseFee: 114, + }) + + t.Run("channel_account_signature_client_get_account_public_key_err", func(t *testing.T) { + channelAccountSignatureClient. + On("GetAccountPublicKey", context.Background()). + Return("", errors.New("channel accounts unavailable")). + Once() + + tx, err := txService.BuildAndSignTransactionWithChannelAccount(context.Background(), []txnbuild.Operation{}, 30) + assert.Empty(t, tx) + assert.Equal(t, "getting channel account public key: channel accounts unavailable", err.Error()) + }) + + t.Run("horizon_client_get_account_detail_err", func(t *testing.T) { + channelAccount := keypair.MustRandom() + channelAccountSignatureClient. + On("GetAccountPublicKey", context.Background()). + Return(channelAccount.Address(), nil). + Once() + + horizonClient. + On("AccountDetail", horizonclient.AccountRequest{ + AccountID: channelAccount.Address(), + }). + Return(horizon.Account{}, errors.New("horizon down")). + Once() + + tx, err := txService.BuildAndSignTransactionWithChannelAccount(context.Background(), []txnbuild.Operation{}, 30) + assert.Empty(t, tx) + assert.Equal(t, "getting channel account details from horizon: horizon down", err.Error()) + }) + + t.Run("build_tx_fails", func(t *testing.T) { + channelAccount := keypair.MustRandom() + channelAccountSignatureClient. + On("GetAccountPublicKey", context.Background()). + Return(channelAccount.Address(), nil). + Once() + + horizonClient. + On("AccountDetail", horizonclient.AccountRequest{ + AccountID: channelAccount.Address(), + }). + Return(horizon.Account{AccountID: channelAccount.Address(), Sequence: 1}, nil). + Once() + + tx, err := txService.BuildAndSignTransactionWithChannelAccount(context.Background(), []txnbuild.Operation{}, 30) + assert.Empty(t, tx) + assert.Equal(t, "building transaction: transaction has no operations", err.Error()) + + }) + + t.Run("sign_stellar_transaction_w_channel_account_err", func(t *testing.T) { + channelAccount := keypair.MustRandom() + channelAccountSignatureClient. + On("GetAccountPublicKey", context.Background()). + Return(channelAccount.Address(), nil). + Once(). + On("SignStellarTransaction", context.Background(), mock.AnythingOfType("*txnbuild.Transaction"), []string{channelAccount.Address()}). + Return(nil, errors.New("unable to sign")). + Once() + + horizonClient. + On("AccountDetail", horizonclient.AccountRequest{ + AccountID: channelAccount.Address(), + }). + Return(horizon.Account{AccountID: channelAccount.Address(), Sequence: 1}, nil). + Once() + + payment := txnbuild.Payment{ + Destination: keypair.MustRandom().Address(), + Amount: "10", + Asset: txnbuild.NativeAsset{}, + SourceAccount: keypair.MustRandom().Address(), + } + tx, err := txService.BuildAndSignTransactionWithChannelAccount(context.Background(), []txnbuild.Operation{&payment}, 30) + assert.Empty(t, tx) + assert.Equal(t, "signing transaction with channel account: unable to sign", err.Error()) + }) + + t.Run("returns_signed_tx", func(t *testing.T) { + signedTx := utils.BuildTestTransaction() + channelAccount := keypair.MustRandom() + channelAccountSignatureClient. + On("GetAccountPublicKey", context.Background()). + Return(channelAccount.Address(), nil). + Once(). + On("SignStellarTransaction", context.Background(), mock.AnythingOfType("*txnbuild.Transaction"), []string{channelAccount.Address()}). + Return(signedTx, nil). + Once() + + horizonClient. + On("AccountDetail", horizonclient.AccountRequest{ + AccountID: channelAccount.Address(), + }). + Return(horizon.Account{AccountID: channelAccount.Address(), Sequence: 1}, nil). + Once() + + payment := txnbuild.Payment{ + Destination: keypair.MustRandom().Address(), + Amount: "10", + Asset: txnbuild.NativeAsset{}, + SourceAccount: keypair.MustRandom().Address(), + } + tx, err := txService.BuildAndSignTransactionWithChannelAccount(context.Background(), []txnbuild.Operation{&payment}, 30) + assert.Equal(t, signedTx, tx) + assert.NoError(t, err) + }) +} + +func TestBuildFeeBumpTransaction(t *testing.T) { + distributionAccountSignatureClient := signing.SignatureClientMock{} + defer distributionAccountSignatureClient.AssertExpectations(t) + channelAccountSignatureClient := signing.SignatureClientMock{} + defer channelAccountSignatureClient.AssertExpectations(t) + horizonClient := horizonclient.MockClient{} + defer horizonClient.AssertExpectations(t) + txService, _ := NewTransactionService(TransactionServiceOptions{ + DistributionAccountSignatureClient: &distributionAccountSignatureClient, + ChannelAccountSignatureClient: &channelAccountSignatureClient, + HorizonClient: &horizonClient, + BaseFee: 114, + }) + + t.Run("distribution_account_signature_client_get_account_public_key_err", func(t *testing.T) { + tx := utils.BuildTestTransaction() + distributionAccountSignatureClient. + On("GetAccountPublicKey", context.Background()). + Return("", errors.New("channel accounts unavailable")). + Once() + + feeBumpTx, err := txService.BuildFeeBumpTransaction(context.Background(), tx) + assert.Empty(t, feeBumpTx) + assert.Equal(t, "getting distribution account public key: channel accounts unavailable", err.Error()) + }) + + t.Run("building_tx_fails", func(t *testing.T) { + distributionAccount := keypair.MustRandom() + distributionAccountSignatureClient. + On("GetAccountPublicKey", context.Background()). + Return(distributionAccount.Address(), nil). + Once() + + feeBumpTx, err := txService.BuildFeeBumpTransaction(context.Background(), nil) + assert.Empty(t, feeBumpTx) + assert.Equal(t, "building fee-bump transaction inner transaction is missing", err.Error()) + }) + + t.Run("signing_feebump_tx_fails", func(t *testing.T) { + tx := utils.BuildTestTransaction() + distributionAccount := keypair.MustRandom() + distributionAccountSignatureClient. + On("GetAccountPublicKey", context.Background()). + Return(distributionAccount.Address(), nil). + Once(). + On("SignStellarFeeBumpTransaction", context.Background(), mock.AnythingOfType("*txnbuild.FeeBumpTransaction")). + Return(nil, errors.New("unable to sign fee bump transaction")). + Once() + + feeBumpTx, err := txService.BuildFeeBumpTransaction(context.Background(), tx) + assert.Empty(t, feeBumpTx) + assert.Equal(t, "signing the fee bump transaction with distribution account: unable to sign fee bump transaction", err.Error()) + }) + + t.Run("returns_singed_feebump_tx", func(t *testing.T) { + tx := utils.BuildTestTransaction() + feeBump := utils.BuildTestFeeBumpTransaction() + distributionAccount := keypair.MustRandom() + distributionAccountSignatureClient. + On("GetAccountPublicKey", context.Background()). + Return(distributionAccount.Address(), nil). + Once(). + On("SignStellarFeeBumpTransaction", context.Background(), mock.AnythingOfType("*txnbuild.FeeBumpTransaction")). + Return(feeBump, nil). + Once() + + feeBumpTx, err := txService.BuildFeeBumpTransaction(context.Background(), tx) + assert.Equal(t, feeBump, feeBumpTx) + assert.NoError(t, err) + }) + +} + +/* func TestSignAndBuildNewFeeBumpTransaction(t *testing.T) { distributionAccountSignatureClient := signing.SignatureClientMock{} defer distributionAccountSignatureClient.AssertExpectations(t) @@ -290,3 +487,4 @@ func TestSignAndBuildNewFeeBumpTransaction(t *testing.T) { assert.Empty(t, err) }) } +*/ diff --git a/internal/tss/types.go b/internal/tss/types.go index b23fa3e..d7de9c5 100644 --- a/internal/tss/types.go +++ b/internal/tss/types.go @@ -201,6 +201,8 @@ type Payload struct { RpcSubmitTxResponse RPCSendTxResponse // Relevant fields in the transaction list inside the RPC getTransactions response RpcGetIngestTxResponse RPCGetIngestTxResponse + // indicates if the transaction to be built from this payload should be wrapped in a fee bump transaction + FeeBump bool } type Channel interface { diff --git a/internal/tss/utils/helpers.go b/internal/tss/utils/helpers.go index dc50d16..1c8e258 100644 --- a/internal/tss/utils/helpers.go +++ b/internal/tss/utils/helpers.go @@ -36,7 +36,7 @@ func BuildTestTransaction() *txnbuild.Transaction { Operations: []txnbuild.Operation{ &txnbuild.Payment{ Destination: keypair.MustRandom().Address(), - Amount: "14", + Amount: "14.0000000", Asset: txnbuild.NativeAsset{}, }, }, diff --git a/internal/tss/utils/operation_builder.go b/internal/tss/utils/operation_builder.go new file mode 100644 index 0000000..783ce62 --- /dev/null +++ b/internal/tss/utils/operation_builder.go @@ -0,0 +1,89 @@ +package utils + +import ( + "bytes" + "encoding/base64" + "fmt" + + xdr3 "github.com/stellar/go-xdr/xdr3" + "github.com/stellar/go/txnbuild" + "github.com/stellar/go/xdr" +) + +func BuildOperations(txOpXDRs []string) ([]txnbuild.Operation, error) { + var operations []txnbuild.Operation + for _, opStr := range txOpXDRs { + decodedBytes, err := base64.StdEncoding.DecodeString(opStr) + if err != nil { + return nil, fmt.Errorf("decoding Operation XDR string") + } + var decodedOp xdr.Operation + _, err = xdr3.Unmarshal(bytes.NewReader(decodedBytes), &decodedOp) + if err != nil { + return nil, fmt.Errorf("decoding xdr into xdr Operation: %w", err) + } + var operation txnbuild.Operation + switch xdr.OperationType(decodedOp.Body.Type) { + case xdr.OperationTypeCreateAccount: + operation = &txnbuild.CreateAccount{} + case xdr.OperationTypePayment: + operation = &txnbuild.Payment{} + case xdr.OperationTypePathPaymentStrictReceive: + operation = &txnbuild.PathPaymentStrictReceive{} + case xdr.OperationTypeManageSellOffer: + operation = &txnbuild.ManageSellOffer{} + case xdr.OperationTypeCreatePassiveSellOffer: + operation = &txnbuild.CreatePassiveSellOffer{} + case xdr.OperationTypeSetOptions: + operation = &txnbuild.SetOptions{} + case xdr.OperationTypeChangeTrust: + operation = &txnbuild.ChangeTrust{} + case xdr.OperationTypeAccountMerge: + operation = &txnbuild.AccountMerge{} + case xdr.OperationTypeInflation: + operation = &txnbuild.Inflation{} + case xdr.OperationTypeManageData: + operation = &txnbuild.ManageData{} + case xdr.OperationTypeBumpSequence: + operation = &txnbuild.BumpSequence{} + case xdr.OperationTypeManageBuyOffer: + operation = &txnbuild.ManageBuyOffer{} + case xdr.OperationTypePathPaymentStrictSend: + operation = &txnbuild.PathPaymentStrictSend{} + case xdr.OperationTypeCreateClaimableBalance: + operation = &txnbuild.CreateClaimableBalance{} + case xdr.OperationTypeClaimClaimableBalance: + operation = &txnbuild.ClaimClaimableBalance{} + case xdr.OperationTypeBeginSponsoringFutureReserves: + operation = &txnbuild.BeginSponsoringFutureReserves{} + case xdr.OperationTypeEndSponsoringFutureReserves: + operation = &txnbuild.EndSponsoringFutureReserves{} + case xdr.OperationTypeRevokeSponsorship: + operation = &txnbuild.RevokeSponsorship{} + case xdr.OperationTypeClawback: + operation = &txnbuild.Clawback{} + case xdr.OperationTypeClawbackClaimableBalance: + operation = &txnbuild.ClawbackClaimableBalance{} + case xdr.OperationTypeSetTrustLineFlags: + operation = &txnbuild.SetTrustLineFlags{} + case xdr.OperationTypeLiquidityPoolDeposit: + operation = &txnbuild.LiquidityPoolDeposit{} + case xdr.OperationTypeLiquidityPoolWithdraw: + operation = &txnbuild.LiquidityPoolWithdraw{} + case xdr.OperationTypeInvokeHostFunction: + operation = &txnbuild.InvokeHostFunction{} + case xdr.OperationTypeExtendFootprintTtl: + operation = &txnbuild.ExtendFootprintTtl{} + case xdr.OperationTypeRestoreFootprint: + operation = &txnbuild.RestoreFootprint{} + default: + return nil, fmt.Errorf("unrecognized op") + } + err = operation.FromXDR(decodedOp) + if err != nil { + return nil, fmt.Errorf("decoding Operation FromXDR") + } + operations = append(operations, operation) + } + return operations, nil +} diff --git a/internal/tss/utils/operation_builder_test.go b/internal/tss/utils/operation_builder_test.go new file mode 100644 index 0000000..ca1039d --- /dev/null +++ b/internal/tss/utils/operation_builder_test.go @@ -0,0 +1,382 @@ +package utils + +import ( + "encoding/base64" + "strings" + "testing" + + xdr3 "github.com/stellar/go-xdr/xdr3" + "github.com/stellar/go/keypair" + "github.com/stellar/go/txnbuild" + "github.com/stellar/go/xdr" + "github.com/stretchr/testify/assert" +) + +func TestBuildOperations(t *testing.T) { + t.Run("op_createaccount", func(t *testing.T) { + srcAccount := keypair.MustRandom().Address() + dstAccount := keypair.MustRandom().Address() + c := txnbuild.CreateAccount{ + Destination: dstAccount, + Amount: "10", + SourceAccount: srcAccount, + } + op, _ := c.BuildXDR() + var buf strings.Builder + enc := xdr3.NewEncoder(&buf) + _ = op.EncodeTo(enc) + opXDR := buf.String() + opXDRBase64 := base64.StdEncoding.EncodeToString([]byte(opXDR)) + + ops, _ := BuildOperations([]string{opXDRBase64}) + + assert.Equal(t, srcAccount, ops[0].GetSourceAccount()) + assert.Equal(t, dstAccount, ops[0].(*txnbuild.CreateAccount).Destination) + assert.Equal(t, string("10.0000000"), ops[0].(*txnbuild.CreateAccount).Amount) + }) + t.Run("op_payment", func(t *testing.T) { + srcAccount := keypair.MustRandom().Address() + dstAccount := keypair.MustRandom().Address() + p := txnbuild.Payment{ + Destination: dstAccount, + 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)) + + ops, _ := BuildOperations([]string{opXDRBase64}) + + assert.Equal(t, srcAccount, ops[0].GetSourceAccount()) + assert.Equal(t, string("10.0000000"), ops[0].(*txnbuild.Payment).Amount) + assert.Equal(t, dstAccount, ops[0].(*txnbuild.Payment).Destination) + assert.Equal(t, txnbuild.NativeAsset{}, ops[0].(*txnbuild.Payment).Asset) + }) + + t.Run("op_manage_sell_offer", func(t *testing.T) { + srcAccount := keypair.MustRandom().Address() + m := txnbuild.ManageSellOffer{ + Selling: txnbuild.NativeAsset{}, + Buying: txnbuild.NativeAsset{}, + Amount: "10", + OfferID: int64(1234), + SourceAccount: srcAccount, + Price: xdr.Price{N: 10, D: 10}, + } + op, _ := m.BuildXDR() + var buf strings.Builder + enc := xdr3.NewEncoder(&buf) + _ = op.EncodeTo(enc) + opXDR := buf.String() + opXDRBase64 := base64.StdEncoding.EncodeToString([]byte(opXDR)) + + ops, _ := BuildOperations([]string{opXDRBase64}) + + assert.Equal(t, srcAccount, ops[0].GetSourceAccount()) + assert.Equal(t, string("10.0000000"), ops[0].(*txnbuild.ManageSellOffer).Amount) + assert.Equal(t, int64(1234), ops[0].(*txnbuild.ManageSellOffer).OfferID) + assert.Equal(t, txnbuild.NativeAsset{}, ops[0].(*txnbuild.ManageSellOffer).Selling) + assert.Equal(t, txnbuild.NativeAsset{}, ops[0].(*txnbuild.ManageSellOffer).Buying) + assert.Equal(t, xdr.Price{N: 10, D: 10}, ops[0].(*txnbuild.ManageSellOffer).Price) + }) + + t.Run("op_create_passive_sell_offer", func(t *testing.T) { + srcAccount := keypair.MustRandom().Address() + c := txnbuild.CreatePassiveSellOffer{ + Selling: txnbuild.NativeAsset{}, + Buying: txnbuild.NativeAsset{}, + Amount: "10", + Price: xdr.Price{N: 10, D: 10}, + SourceAccount: srcAccount, + } + op, _ := c.BuildXDR() + var buf strings.Builder + enc := xdr3.NewEncoder(&buf) + _ = op.EncodeTo(enc) + opXDR := buf.String() + opXDRBase64 := base64.StdEncoding.EncodeToString([]byte(opXDR)) + + ops, _ := BuildOperations([]string{opXDRBase64}) + + assert.Equal(t, srcAccount, ops[0].GetSourceAccount()) + assert.Equal(t, string("10.0000000"), ops[0].(*txnbuild.CreatePassiveSellOffer).Amount) + assert.Equal(t, txnbuild.NativeAsset{}, ops[0].(*txnbuild.CreatePassiveSellOffer).Selling) + assert.Equal(t, txnbuild.NativeAsset{}, ops[0].(*txnbuild.CreatePassiveSellOffer).Buying) + assert.Equal(t, xdr.Price{N: 10, D: 10}, ops[0].(*txnbuild.CreatePassiveSellOffer).Price) + }) + + t.Run("op_set_options", func(t *testing.T) { + srcAccount := keypair.MustRandom().Address() + s := txnbuild.SetOptions{ + SourceAccount: srcAccount, + } + op, _ := s.BuildXDR() + var buf strings.Builder + enc := xdr3.NewEncoder(&buf) + _ = op.EncodeTo(enc) + opXDR := buf.String() + opXDRBase64 := base64.StdEncoding.EncodeToString([]byte(opXDR)) + + ops, _ := BuildOperations([]string{opXDRBase64}) + + assert.Equal(t, srcAccount, ops[0].GetSourceAccount()) + }) + + t.Run("op_account_merge", func(t *testing.T) { + srcAccount := keypair.MustRandom().Address() + dstAccount := keypair.MustRandom().Address() + a := txnbuild.AccountMerge{ + Destination: dstAccount, + SourceAccount: srcAccount, + } + op, _ := a.BuildXDR() + var buf strings.Builder + enc := xdr3.NewEncoder(&buf) + _ = op.EncodeTo(enc) + opXDR := buf.String() + opXDRBase64 := base64.StdEncoding.EncodeToString([]byte(opXDR)) + + ops, _ := BuildOperations([]string{opXDRBase64}) + + assert.Equal(t, srcAccount, ops[0].GetSourceAccount()) + assert.Equal(t, dstAccount, ops[0].(*txnbuild.AccountMerge).Destination) + }) + + t.Run("op_inflation", func(t *testing.T) { + srcAccount := keypair.MustRandom().Address() + i := txnbuild.Inflation{ + SourceAccount: srcAccount, + } + op, _ := i.BuildXDR() + var buf strings.Builder + enc := xdr3.NewEncoder(&buf) + _ = op.EncodeTo(enc) + opXDR := buf.String() + opXDRBase64 := base64.StdEncoding.EncodeToString([]byte(opXDR)) + + ops, _ := BuildOperations([]string{opXDRBase64}) + + assert.Equal(t, srcAccount, ops[0].GetSourceAccount()) + }) + + t.Run("op_manage_data", func(t *testing.T) { + srcAccount := keypair.MustRandom().Address() + m := txnbuild.ManageData{ + Name: "foo", + SourceAccount: srcAccount, + } + op, _ := m.BuildXDR() + var buf strings.Builder + enc := xdr3.NewEncoder(&buf) + _ = op.EncodeTo(enc) + opXDR := buf.String() + opXDRBase64 := base64.StdEncoding.EncodeToString([]byte(opXDR)) + + ops, _ := BuildOperations([]string{opXDRBase64}) + + assert.Equal(t, srcAccount, ops[0].GetSourceAccount()) + assert.Equal(t, "foo", ops[0].(*txnbuild.ManageData).Name) + }) + + t.Run("op_bump_sequence", func(t *testing.T) { + srcAccount := keypair.MustRandom().Address() + b := txnbuild.BumpSequence{ + BumpTo: int64(100), + SourceAccount: srcAccount, + } + op, _ := b.BuildXDR() + var buf strings.Builder + enc := xdr3.NewEncoder(&buf) + _ = op.EncodeTo(enc) + opXDR := buf.String() + opXDRBase64 := base64.StdEncoding.EncodeToString([]byte(opXDR)) + + ops, _ := BuildOperations([]string{opXDRBase64}) + + assert.Equal(t, srcAccount, ops[0].GetSourceAccount()) + assert.Equal(t, int64(100), ops[0].(*txnbuild.BumpSequence).BumpTo) + }) + + t.Run("op_manage_buy_offer", func(t *testing.T) { + srcAccount := keypair.MustRandom().Address() + m := txnbuild.ManageBuyOffer{ + Selling: txnbuild.NativeAsset{}, + Buying: txnbuild.NativeAsset{}, + Amount: "10", + Price: xdr.Price{N: 10, D: 10}, + OfferID: int64(100), + SourceAccount: srcAccount, + } + op, _ := m.BuildXDR() + var buf strings.Builder + enc := xdr3.NewEncoder(&buf) + _ = op.EncodeTo(enc) + opXDR := buf.String() + opXDRBase64 := base64.StdEncoding.EncodeToString([]byte(opXDR)) + + ops, _ := BuildOperations([]string{opXDRBase64}) + + assert.Equal(t, srcAccount, ops[0].GetSourceAccount()) + assert.Equal(t, txnbuild.NativeAsset{}, ops[0].(*txnbuild.ManageBuyOffer).Selling) + assert.Equal(t, txnbuild.NativeAsset{}, ops[0].(*txnbuild.ManageBuyOffer).Buying) + assert.Equal(t, string("10.0000000"), ops[0].(*txnbuild.ManageBuyOffer).Amount) + assert.Equal(t, xdr.Price{N: 10, D: 10}, ops[0].(*txnbuild.ManageBuyOffer).Price) + assert.Equal(t, int64(100), ops[0].(*txnbuild.ManageBuyOffer).OfferID) + }) + + t.Run("op_path_payment_strict_send", func(t *testing.T) { + srcAccount := keypair.MustRandom().Address() + dstAccount := keypair.MustRandom().Address() + p := txnbuild.PathPaymentStrictSend{ + SendAsset: txnbuild.NativeAsset{}, + SendAmount: "10", + Destination: dstAccount, + DestAsset: txnbuild.NativeAsset{}, + DestMin: "1", + Path: []txnbuild.Asset{}, + 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)) + + ops, _ := BuildOperations([]string{opXDRBase64}) + + assert.Equal(t, srcAccount, ops[0].GetSourceAccount()) + assert.Equal(t, txnbuild.NativeAsset{}, ops[0].(*txnbuild.PathPaymentStrictSend).SendAsset) + assert.Equal(t, "10.0000000", ops[0].(*txnbuild.PathPaymentStrictSend).SendAmount) + assert.Equal(t, dstAccount, ops[0].(*txnbuild.PathPaymentStrictSend).Destination) + assert.Equal(t, txnbuild.NativeAsset{}, ops[0].(*txnbuild.PathPaymentStrictSend).DestAsset) + assert.Equal(t, "1.0000000", ops[0].(*txnbuild.PathPaymentStrictSend).DestMin) + assert.Equal(t, []txnbuild.Asset{}, ops[0].(*txnbuild.PathPaymentStrictSend).Path) + }) + + t.Run("op_create_claimable_balance", func(t *testing.T) { + srcAccount := keypair.MustRandom().Address() + c := txnbuild.CreateClaimableBalance{ + Amount: "10", + Asset: txnbuild.NativeAsset{}, + SourceAccount: srcAccount, + } + op, _ := c.BuildXDR() + var buf strings.Builder + enc := xdr3.NewEncoder(&buf) + _ = op.EncodeTo(enc) + opXDR := buf.String() + opXDRBase64 := base64.StdEncoding.EncodeToString([]byte(opXDR)) + + ops, _ := BuildOperations([]string{opXDRBase64}) + + assert.Equal(t, srcAccount, ops[0].GetSourceAccount()) + assert.Equal(t, "10.0000000", ops[0].(*txnbuild.CreateClaimableBalance).Amount) + assert.Equal(t, txnbuild.NativeAsset{}, ops[0].(*txnbuild.CreateClaimableBalance).Asset) + }) + + t.Run("op_end_sponsoring_future_reserves", func(t *testing.T) { + srcAccount := keypair.MustRandom().Address() + e := txnbuild.EndSponsoringFutureReserves{ + SourceAccount: srcAccount, + } + op, _ := e.BuildXDR() + var buf strings.Builder + enc := xdr3.NewEncoder(&buf) + _ = op.EncodeTo(enc) + opXDR := buf.String() + opXDRBase64 := base64.StdEncoding.EncodeToString([]byte(opXDR)) + + ops, _ := BuildOperations([]string{opXDRBase64}) + + assert.Equal(t, srcAccount, ops[0].GetSourceAccount()) + }) + t.Run("op_liquidity_pool_deposit", func(t *testing.T) { + srcAccount := keypair.MustRandom().Address() + l := txnbuild.LiquidityPoolDeposit{ + SourceAccount: srcAccount, + MaxAmountA: "10", + MaxAmountB: "10", + MinPrice: xdr.Price{N: 10, D: 10}, + MaxPrice: xdr.Price{N: 10, D: 10}, + } + op, _ := l.BuildXDR() + var buf strings.Builder + enc := xdr3.NewEncoder(&buf) + _ = op.EncodeTo(enc) + opXDR := buf.String() + opXDRBase64 := base64.StdEncoding.EncodeToString([]byte(opXDR)) + + ops, _ := BuildOperations([]string{opXDRBase64}) + + assert.Equal(t, srcAccount, ops[0].GetSourceAccount()) + assert.Equal(t, "10.0000000", ops[0].(*txnbuild.LiquidityPoolDeposit).MaxAmountA) + assert.Equal(t, "10.0000000", ops[0].(*txnbuild.LiquidityPoolDeposit).MaxAmountB) + assert.Equal(t, xdr.Price{N: 10, D: 10}, ops[0].(*txnbuild.LiquidityPoolDeposit).MinPrice) + assert.Equal(t, xdr.Price{N: 10, D: 10}, ops[0].(*txnbuild.LiquidityPoolDeposit).MaxPrice) + }) + + t.Run("op_liquidity_pool_withdraw", func(t *testing.T) { + srcAccount := keypair.MustRandom().Address() + l := txnbuild.LiquidityPoolWithdraw{ + SourceAccount: srcAccount, + Amount: "10", + MinAmountA: "10", + MinAmountB: "10", + } + op, _ := l.BuildXDR() + var buf strings.Builder + enc := xdr3.NewEncoder(&buf) + _ = op.EncodeTo(enc) + opXDR := buf.String() + opXDRBase64 := base64.StdEncoding.EncodeToString([]byte(opXDR)) + + ops, _ := BuildOperations([]string{opXDRBase64}) + + assert.Equal(t, srcAccount, ops[0].GetSourceAccount()) + assert.Equal(t, "10.0000000", ops[0].(*txnbuild.LiquidityPoolWithdraw).Amount) + assert.Equal(t, "10.0000000", ops[0].(*txnbuild.LiquidityPoolWithdraw).MinAmountA) + assert.Equal(t, "10.0000000", ops[0].(*txnbuild.LiquidityPoolWithdraw).MinAmountB) + }) + + t.Run("op_extend_footprint_ttl", func(t *testing.T) { + srcAccount := keypair.MustRandom().Address() + e := txnbuild.ExtendFootprintTtl{ + ExtendTo: uint32(10), + SourceAccount: srcAccount, + } + op, _ := e.BuildXDR() + var buf strings.Builder + enc := xdr3.NewEncoder(&buf) + _ = op.EncodeTo(enc) + opXDR := buf.String() + opXDRBase64 := base64.StdEncoding.EncodeToString([]byte(opXDR)) + + ops, _ := BuildOperations([]string{opXDRBase64}) + + assert.Equal(t, srcAccount, ops[0].GetSourceAccount()) + }) + + t.Run("op_restore_footprint", func(t *testing.T) { + srcAccount := keypair.MustRandom().Address() + r := txnbuild.RestoreFootprint{ + SourceAccount: srcAccount, + } + op, _ := r.BuildXDR() + var buf strings.Builder + enc := xdr3.NewEncoder(&buf) + _ = op.EncodeTo(enc) + opXDR := buf.String() + opXDRBase64 := base64.StdEncoding.EncodeToString([]byte(opXDR)) + + ops, _ := BuildOperations([]string{opXDRBase64}) + + assert.Equal(t, srcAccount, ops[0].GetSourceAccount()) + }) + +} diff --git a/internal/tss/utils/transaction_builder.go b/internal/tss/utils/transaction_builder.go deleted file mode 100644 index 8958313..0000000 --- a/internal/tss/utils/transaction_builder.go +++ /dev/null @@ -1,51 +0,0 @@ -package utils - -import ( - "bytes" - "encoding/base64" - "fmt" - - xdr3 "github.com/stellar/go-xdr/xdr3" - "github.com/stellar/go/keypair" - "github.com/stellar/go/txnbuild" - "github.com/stellar/go/xdr" -) - -func BuildOriginalTransaction(txOpXDRs []string) (*txnbuild.Transaction, error) { - var operations []txnbuild.Operation - for _, opXDR := range txOpXDRs { - decodedBytes, err := base64.StdEncoding.DecodeString(opXDR) - if err != nil { - return nil, fmt.Errorf("decoding Operation XDR string") - } - var decodedOp xdr.Operation - _, err = xdr3.Unmarshal(bytes.NewReader(decodedBytes), &decodedOp) - - if err != nil { - return nil, fmt.Errorf("decoding xdr into xdr Operation: %w", err) - } - // for now, we assume that all operations are Payment operations - paymentOp := txnbuild.Payment{} - err = paymentOp.FromXDR(decodedOp) - if err != nil { - return nil, fmt.Errorf("unmarshaling xdr into Operation: %w", err) - } - err = paymentOp.Validate() - if err != nil { - return nil, fmt.Errorf("invalid Operation: %w", err) - } - operations = append(operations, &paymentOp) - } - - tx, err := txnbuild.NewTransaction(txnbuild.TransactionParams{ - SourceAccount: &txnbuild.SimpleAccount{ - AccountID: keypair.MustRandom().Address(), - }, - Operations: operations, - Preconditions: txnbuild.Preconditions{TimeBounds: txnbuild.NewTimeout(10)}, - }) - if err != nil { - return nil, fmt.Errorf("cannot create new transaction: %w", err) - } - return tx, nil -} diff --git a/internal/tss/utils/transaction_builder_test.go b/internal/tss/utils/transaction_builder_test.go deleted file mode 100644 index 0d8897f..0000000 --- a/internal/tss/utils/transaction_builder_test.go +++ /dev/null @@ -1,61 +0,0 @@ -package utils - -import ( - "encoding/base64" - "strings" - "testing" - - xdr3 "github.com/stellar/go-xdr/xdr3" - "github.com/stellar/go/keypair" - "github.com/stellar/go/txnbuild" - "github.com/stretchr/testify/assert" -) - -func TestBuildOriginalTransaction(t *testing.T) { - t.Run("return_error_when_unable_to_decode_operation_xdr_string", func(t *testing.T) { - _, err := BuildOriginalTransaction([]string{"this@is#not$valid!"}) - assert.Equal(t, "decoding Operation XDR string", err.Error()) - - }) - t.Run("return_error_when_unable_to_unmarshal_xdr_into_operation", func(t *testing.T) { - ca := txnbuild.CreateAccount{ - Destination: keypair.MustRandom().Address(), - Amount: "10", - SourceAccount: keypair.MustRandom().Address(), - } - caOp, _ := ca.BuildXDR() - - buf := strings.Builder{} - enc := xdr3.NewEncoder(&buf) - _ = caOp.EncodeTo(enc) - - caOpXDR := buf.String() - caOpXDRBase64 := base64.StdEncoding.EncodeToString([]byte(caOpXDR)) - - _, err := BuildOriginalTransaction([]string{caOpXDRBase64}) - assert.Equal(t, "unmarshaling xdr into Operation: error parsing payment operation from xdr", err.Error()) - }) - - t.Run("return_expected_transaction", 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, err := BuildOriginalTransaction([]string{opXDRBase64}) - firstOp := tx.Operations()[0] - assert.Equal(t, firstOp.GetSourceAccount(), srcAccount) - assert.Empty(t, err) - }) - -}