Skip to content

Commit

Permalink
feat: add broadcast amino support (#19)
Browse files Browse the repository at this point in the history
Co-authored-by: Federico Kunze Küllmer <[email protected]>
  • Loading branch information
facs95 and fedekunze authored Jul 6, 2023
1 parent 492fba3 commit 869fc4f
Show file tree
Hide file tree
Showing 9 changed files with 185 additions and 18 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
go-backend
dashboard-backend
.env
server
1 change: 1 addition & 0 deletions api/handler/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 0 additions & 1 deletion api/handler/v2/delegations.go
Original file line number Diff line number Diff line change
@@ -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 (
Expand Down
154 changes: 154 additions & 0 deletions api/handler/v2/tx.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand All @@ -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:
Expand Down Expand Up @@ -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())
Expand All @@ -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
}
19 changes: 12 additions & 7 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
version: "3.2"
services:
cron:
container_name: cronjobs
build:
context: ./cronjobs
dockerfile: dockerfile
Expand All @@ -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
Expand All @@ -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:
Expand All @@ -43,11 +50,9 @@ services:
- dashboard-backend-api
ports:
- "80:80"

dashboard-backend-redis:
image: redis
ports:
- "6379:6379"

volumes:
app-volume:
21 changes: 12 additions & 9 deletions internal/v1/db/endpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
package db

import (
"context"
"strings"
)

Expand All @@ -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
}

Expand Down
4 changes: 4 additions & 0 deletions internal/v2/node/rest/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
1 change: 0 additions & 1 deletion internal/v2/numia/client.go
Original file line number Diff line number Diff line change
@@ -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 (
Expand Down
Binary file removed server
Binary file not shown.

0 comments on commit 869fc4f

Please sign in to comment.