diff --git a/.gitignore b/.gitignore index ae7f2d6..4a43b9b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ go-backend dashboard-backend +.env +server diff --git a/api/handler/router.go b/api/handler/router.go index c43fab5..478258f 100644 --- a/api/handler/router.go +++ b/api/handler/router.go @@ -18,6 +18,7 @@ func (h *Handler) RegisterRoutes(r *router.Router) { // Tx endpoints r.POST("/v2/tx/broadcast", h.v2.BroadcastTx) + r.POST("/v2/tx/amino/broadcast", h.v2.BroadcastAminoTx) // v1 endpoints to be deprecated // NOTE: v1 endpoints do not have a /v1 prefix for backwards compatibility diff --git a/api/handler/v2/delegations.go b/api/handler/v2/delegations.go index 2186daa..ec70105 100644 --- a/api/handler/v2/delegations.go +++ b/api/handler/v2/delegations.go @@ -1,6 +1,5 @@ // Copyright Tharsis Labs Ltd.(Evmos) // SPDX-License-Identifier:ENCL-1.0(https://github.com/evmos/backend/blob/main/LICENSE) - package v2 import ( diff --git a/api/handler/v2/tx.go b/api/handler/v2/tx.go index 0154435..b3012ca 100644 --- a/api/handler/v2/tx.go +++ b/api/handler/v2/tx.go @@ -2,10 +2,15 @@ package v2 import ( "encoding/json" + "fmt" + "github.com/tharsis/dashboard-backend/internal/v2/encoding" "github.com/tharsis/dashboard-backend/internal/v2/node/rest" + "github.com/cosmos/cosmos-sdk/simapp/params" + sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/tx" + "github.com/cosmos/cosmos-sdk/x/auth/migrations/legacytx" "github.com/valyala/fasthttp" ) @@ -17,12 +22,28 @@ type BroadcastTxParams struct { TxBytes []byte `json:"tx_bytes"` } +// BroadcastTxResponse represents the response for the POST /v2/tx/broadcast endpoint. type BroadcastTxResponse struct { Code uint32 `json:"code"` TxHash string `json:"tx_hash"` RawLog string `json:"raw_log"` } +// BroadcastTxParams represents the parameters for the POST /v2/tx/broadcast endpoint. +type BroadcastAminoTxParams struct { + // which network should the transaction be broadcasted to + Network string `json:"network"` + Signed legacytx.StdSignDoc `json:"signed"` + Signature legacytx.StdSignature `json:"signature"` //nolint:staticcheck +} + +// BroadcastTxResponse represents the response for the POST /v2/tx/amion/broadcast endpoint. +type BroadcastAminoTxResponse struct { + Code uint32 `json:"code"` + TxHash string `json:"tx_hash"` + RawLog string `json:"raw_log"` +} + // BroadcastTx handles POST /tx/broadcast. // It broadcasts a signed transaction synchronously to the specified network. // Returns: @@ -52,6 +73,12 @@ func (h *Handler) BroadcastTx(ctx *fasthttp.RequestCtx) { return } + err = ValidateBroadcastTxParams(&reqParams) + if err != nil { + sendBadRequestResponse(ctx, err.Error()) + return + } + restClient, err := rest.NewClient(reqParams.Network) if err != nil { ctx.Logger().Printf("Error creating rest client: %s", err.Error()) @@ -73,3 +100,130 @@ func (h *Handler) BroadcastTx(ctx *fasthttp.RequestCtx) { } sendSuccessfulJSONResponse(ctx, &response) } + +// ValidateBroadcastTxParams validates the parameters for the POST /v2/tx/broadcast endpoint. +func ValidateBroadcastTxParams(params *BroadcastTxParams) error { + // TODO: validate network by checking if it's in the list of available networks + if params.Network == "" { + return fmt.Errorf("network cannot be empty") + } + if len(params.TxBytes) == 0 { + return fmt.Errorf("tx_bytes cannot be empty") + } + return nil +} + +// BroadcastAminoTx handles POST /tx/amino/broadcast. +// It broadcasts a signed transaction synchronously to the specified network. +// It receives StdSignDoc and StdSignature as input and builds a TxBuilder to generate +// the broadcast bytes. +// Returns: +// +// { +// "txhash": "3CB7FCC9F5FB31E530CC15665F3FD655AE6CB56CDACAD58D1395C68EDD50D0BB", +// "code": 0, +// "raw_log": "[]", +// } +func (h *Handler) BroadcastAminoTx(ctx *fasthttp.RequestCtx) { + protoCfg := encoding.MakeEncodingConfig() + aminoCodec := protoCfg.Amino + + reqParams := BroadcastAminoTxParams{} + if err := aminoCodec.Amino.UnmarshalJSON(ctx.PostBody(), &reqParams); err != nil { + ctx.Logger().Printf("Error decoding request body: %s", err.Error()) + sendBadRequestResponse(ctx, "Invalid request body") + return + } + + txBytes, err := EncodeLegacyTransaction(&protoCfg, reqParams.Signed, reqParams.Signature) + if err != nil { + ctx.Logger().Printf("Error generating tx bytes: %s", err.Error()) + sendInternalErrorResponse(ctx) + return + } + + txRequest := tx.BroadcastTxRequest{ + TxBytes: txBytes, + Mode: tx.BroadcastMode_BROADCAST_MODE_SYNC, + } + + jsonTxRequest, err := json.Marshal(txRequest) + if err != nil { + ctx.Logger().Printf("Error marshaling txRequest: %s", err.Error()) + sendInternalErrorResponse(ctx) + return + } + + restClient, err := rest.NewClient(reqParams.Network) + if err != nil { + ctx.Logger().Printf("Error creating rest client: %s", err.Error()) + sendInternalErrorResponse(ctx) + return + } + + txResponse, err := restClient.BroadcastTx(jsonTxRequest) + if err != nil { + ctx.Logger().Printf("Error broadcasting tx: %s", err.Error()) + sendInternalErrorResponse(ctx) + return + } + + response := BroadcastTxResponse{ + Code: txResponse.TxResponse.Code, + TxHash: txResponse.TxResponse.TxHash, + RawLog: txResponse.TxResponse.RawLog, + } + sendSuccessfulJSONResponse(ctx, &response) +} + +// EncodeLegacyTransaction encodes the upcoming transaction using the provided configuration. +// It receives StdSignDoc and StdSignature as input and builds a TxBuilder to generate +// the broadcast bytes. +func EncodeLegacyTransaction(encConfig *params.EncodingConfig, signDoc legacytx.StdSignDoc, signature legacytx.StdSignature) ([]byte, error) { //nolint:staticcheck + txBuilder := encConfig.TxConfig.NewTxBuilder() + aminoCodec := encConfig.Amino + var fees legacytx.StdFee + if err := aminoCodec.UnmarshalJSON(signDoc.Fee, &fees); err != nil { + return nil, err + } + + // Validate payload messages + msgs := make([]sdk.Msg, len(signDoc.Msgs)) + for i, jsonMsg := range signDoc.Msgs { + var m sdk.Msg + if err := aminoCodec.UnmarshalJSON(jsonMsg, &m); err != nil { + return nil, err + } + msgs[i] = m + } + + err := txBuilder.SetMsgs(msgs...) + if err != nil { + return nil, err + } + + // Build transaction params + txBuilder.SetMemo(signDoc.Memo) + txBuilder.SetFeeAmount(fees.Amount) + txBuilder.SetFeePayer(sdk.AccAddress(fees.Payer)) + txBuilder.SetFeeGranter(sdk.AccAddress(fees.Granter)) + txBuilder.SetGasLimit(fees.Gas) + txBuilder.SetTimeoutHeight(signDoc.TimeoutHeight) + + sigV2, err := legacytx.StdSignatureToSignatureV2(aminoCodec, signature) + if err != nil { + return nil, err + } + sigV2.Sequence = signDoc.Sequence + + err = txBuilder.SetSignatures(sigV2) + if err != nil { + return nil, err + } + + txBytes, err := encConfig.TxConfig.TxEncoder()(txBuilder.GetTx()) + if err != nil { + return nil, err + } + return txBytes, nil +} diff --git a/docker-compose.yml b/docker-compose.yml index 61e9790..84eca3f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,7 @@ version: "3.2" services: cron: + container_name: cronjobs build: context: ./cronjobs dockerfile: dockerfile @@ -9,10 +10,12 @@ services: - REDIS_HOST=dashboard-backend-redis - REDIS_PORT=6379 - ENV=DEV + - GITHUB_KEY + - ENVIRONMENT=staging depends_on: - dashboard-backend-redis - price: + container_name: price build: context: ./cronjobs dockerfile: dockerfile @@ -21,19 +24,23 @@ services: - REDIS_HOST=dashboard-backend-redis - REDIS_PORT=6379 - ENV=DEV + - GITHUB_KEY + - ENVIRONMENT depends_on: - dashboard-backend-redis - dashboard-backend-api: + container_name: backend build: context: . dockerfile: dockerfile environment: - REDIS_HOST=dashboard-backend-redis + - NUMIA_API_KEY + - NUMIA_RPC_ENDPOINT + - GITHUB_KEY + - ENVIRONMENT=staging depends_on: - - price - - cron - + - dashboard-backend-redis nginx: container_name: nginx build: @@ -43,11 +50,9 @@ services: - dashboard-backend-api ports: - "80:80" - dashboard-backend-redis: image: redis ports: - "6379:6379" - volumes: app-volume: diff --git a/internal/v1/db/endpoint.go b/internal/v1/db/endpoint.go index 7264992..7ed32c0 100644 --- a/internal/v1/db/endpoint.go +++ b/internal/v1/db/endpoint.go @@ -3,6 +3,7 @@ package db import ( + "context" "strings" ) @@ -26,20 +27,22 @@ func RedisGetEndpoint(chain, endpoint, index string) (string, error) { } func RedisGetEndpoints(chain, serverType string) ([]string, error) { - key := buildKeyEndpoint(chain, serverType, "*") - keys, _, err := rdb.Scan(ctxRedis, 0, key, int64(0)).Result() - if err != nil { - return nil, err - } - - nodes := make([]string, len(keys)) - for _, key := range keys { - rd, err := rdb.Get(ctxRedis, key).Result() + ctx := context.Background() + match := buildKeyEndpoint(chain, serverType, "*") + iter := rdb.Scan(ctx, 0, match, 0).Iterator() + var nodes []string + for iter.Next(ctx) { + rd, err := rdb.Get(ctx, iter.Val()).Result() if err != nil { return nil, err } nodes = append(nodes, rd) } + + if err := iter.Err(); err != nil { + return nil, err + } + return nodes, nil } diff --git a/internal/v2/node/rest/client.go b/internal/v2/node/rest/client.go index 4a9e6a7..f1b1ac5 100644 --- a/internal/v2/node/rest/client.go +++ b/internal/v2/node/rest/client.go @@ -47,6 +47,10 @@ func (c *Client) post(endpoint string, body []byte) ([]byte, error) { return bz, nil } +type BadRequestError struct { + Message string `json:"message"` +} + // postRequestWithRetries performs a POST request to the provided URL with the provided body. // It will retry the request with the next available node if the request fails. func (c *Client) postRequestWithRetries(endpoint string, body []byte) (*http.Response, error) { diff --git a/internal/v2/numia/client.go b/internal/v2/numia/client.go index 064f8f1..03b2599 100644 --- a/internal/v2/numia/client.go +++ b/internal/v2/numia/client.go @@ -1,6 +1,5 @@ // Copyright Tharsis Labs Ltd.(Evmos) // SPDX-License-Identifier:ENCL-1.0(https://github.com/evmos/backend/blob/main/LICENSE) - package numia import ( diff --git a/server b/server deleted file mode 100755 index 752a970..0000000 Binary files a/server and /dev/null differ