From 702ea89c7eb98def23e00becabad1c4f80e0d508 Mon Sep 17 00:00:00 2001 From: Frojdi Dymylja Date: Mon, 1 Mar 2021 11:45:56 +0100 Subject: [PATCH 01/33] change(rosetta): data api block and tx parsing --- Makefile | 2 +- contrib/rosetta/configuration/bootstrap.json | 2 +- contrib/rosetta/configuration/data.sh | 2 +- contrib/rosetta/configuration/staking.ros | 4 +- contrib/rosetta/node/data.tar.gz | Bin 36558 -> 36798 bytes server/rosetta/client_v2.go | 299 +++++++++++++++++++ server/rosetta/client_v2_test.go | 47 +++ 7 files changed, 351 insertions(+), 5 deletions(-) create mode 100644 server/rosetta/client_v2.go create mode 100644 server/rosetta/client_v2_test.go diff --git a/Makefile b/Makefile index 87c842b710a6..10725b24e728 100644 --- a/Makefile +++ b/Makefile @@ -313,7 +313,7 @@ test-cover: test-rosetta: docker build -t rosetta-ci:latest -f contrib/rosetta/node/Dockerfile . - docker-compose -f contrib/rosetta/docker-compose.yaml up --abort-on-container-exit --exit-code-from test_rosetta --build + docker-compose -f contrib/rosetta/docker-compose.yaml up .PHONY: test-rosetta benchmark: diff --git a/contrib/rosetta/configuration/bootstrap.json b/contrib/rosetta/configuration/bootstrap.json index 1793988f37e5..401bc4551253 100644 --- a/contrib/rosetta/configuration/bootstrap.json +++ b/contrib/rosetta/configuration/bootstrap.json @@ -1,7 +1,7 @@ [ { "account_identifier": { - "address":"cosmos158nkd0l9tyemv2crp579rmj8dg37qty8lzff88" + "address":"cosmos19g9cm8ymzchq2qkcdv3zgqtwayj9asv3hjv5u5" }, "currency":{ "symbol":"stake", diff --git a/contrib/rosetta/configuration/data.sh b/contrib/rosetta/configuration/data.sh index 45297d5a21bf..4d7d5ff0b056 100644 --- a/contrib/rosetta/configuration/data.sh +++ b/contrib/rosetta/configuration/data.sh @@ -45,7 +45,7 @@ sleep 10 # send transaction to deterministic address echo sending transaction with addr $addr -simd tx bank send "$addr" cosmos1wjmt63j4fv9nqda92nsrp2jp2vsukcke4va3pt 100stake --yes --keyring-backend=test --broadcast-mode=block --chain-id=testing +simd tx bank send "$addr" cosmos19g9cm8ymzchq2qkcdv3zgqtwayj9asv3hjv5u5 100stake --yes --keyring-backend=test --broadcast-mode=block --chain-id=testing sleep 10 diff --git a/contrib/rosetta/configuration/staking.ros b/contrib/rosetta/configuration/staking.ros index 4f89a43b9893..fba5e79a73b1 100644 --- a/contrib/rosetta/configuration/staking.ros +++ b/contrib/rosetta/configuration/staking.ros @@ -94,7 +94,7 @@ staking(1){ "account": { "address": "staking_account", "sub_account": { - "address" : "cosmosvaloper158nkd0l9tyemv2crp579rmj8dg37qty86kaut5" + "address" : "cosmosvaloper19g9cm8ymzchq2qkcdv3zgqtwayj9asv3jxcps8" } }, "amount":{ @@ -134,7 +134,7 @@ staking(1){ "account": { "address": "staking_account", "sub_account": { - "address" : "cosmosvaloper158nkd0l9tyemv2crp579rmj8dg37qty86kaut5" + "address" : "cosmosvaloper19g9cm8ymzchq2qkcdv3zgqtwayj9asv3jxcps8" } }, "amount":{ diff --git a/contrib/rosetta/node/data.tar.gz b/contrib/rosetta/node/data.tar.gz index ad285ac62e75198c4945859e12ccf7b563c7c29b..721cb90aaa1bcdf2fe9251a24cc72c496eaa4c06 100644 GIT binary patch literal 36798 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 W{ds@hpZDkg{P{11*Qv_@PyztMa6X;@ diff --git a/server/rosetta/client_v2.go b/server/rosetta/client_v2.go new file mode 100644 index 000000000000..b36cf8ef1402 --- /dev/null +++ b/server/rosetta/client_v2.go @@ -0,0 +1,299 @@ +package rosetta + +import ( + "context" + "crypto/sha256" + "encoding/json" + "fmt" + + rosettatypes "github.com/coinbase/rosetta-sdk-go/types" + + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/gogo/protobuf/proto" + crgerrs "github.com/tendermint/cosmos-rosetta-gateway/errors" + abci "github.com/tendermint/tendermint/abci/types" + tmtypes "github.com/tendermint/tendermint/types" + + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" +) + +const mockBeginEndBlockTxLength = sha256.Size + 1 +const endBlockHashStart = 0x0 +const beginBlockHashStart = 0x1 +const burnerAddressIdentifier = "burner" + +func (c *Client) blockTxs(ctx context.Context, height *int64) ([]*rosettatypes.Transaction, error) { + // get block info + blockInfo, err := c.clientCtx.Client.Block(ctx, height) + if err != nil { + return nil, err + } + // get block events + blockResults, err := c.clientCtx.Client.BlockResults(ctx, height) + if err != nil { + return nil, err + } + + if len(blockResults.TxsResults) != len(blockInfo.Block.Txs) { + // wtf? + panic("block results transactions do now match block transactions") + } + // process begin and end block txs + beginBlockTx := &rosettatypes.Transaction{ + TransactionIdentifier: &rosettatypes.TransactionIdentifier{Hash: beginBlockTxHash(blockInfo.BlockID.Hash)}, + Operations: normalizeOperationIndexes( + nil, + eventsToBalanceOperations(StatusSuccess, blockResults.BeginBlockEvents), + ), + } + endBlockTx := &rosettatypes.Transaction{ + TransactionIdentifier: &rosettatypes.TransactionIdentifier{Hash: endBlockTxHash(blockInfo.BlockID.Hash)}, + Operations: normalizeOperationIndexes( + nil, + eventsToBalanceOperations(StatusSuccess, blockResults.EndBlockEvents), + ), + } + + deliverTx := make([]*rosettatypes.Transaction, len(blockInfo.Block.Txs)) + // process normal txs + for i, tx := range blockInfo.Block.Txs { + rosTx, err := toRosTx(c.clientCtx.TxConfig.TxDecoder(), tx, blockResults.TxsResults[i]) + if err != nil { + return nil, err + } + deliverTx[i] = rosTx + } + + finalTxs := make([]*rosettatypes.Transaction, 0, 2+len(deliverTx)) + finalTxs = append(finalTxs, beginBlockTx) + finalTxs = append(finalTxs, deliverTx...) + finalTxs = append(finalTxs, endBlockTx) + + return finalTxs, nil +} + +func endBlockTxHash(hash []byte) string { + return fmt.Sprintf("%x%x", endBlockHashStart, hash) +} + +func beginBlockTxHash(hash []byte) string { + return fmt.Sprintf("%x%x", beginBlockHashStart, hash) +} + +func eventsToBalanceOperations(status string, events []abci.Event) []*rosettatypes.Operation { + var ops []*rosettatypes.Operation + + for _, e := range events { + balanceOps, ok := eventToBalanceOperations(status, e) + if !ok { + continue + } + ops = append(ops, balanceOps...) + } + + return ops +} + +// eventToBalanceOperations converts an event to a rosetta balance operation +// it will panic if the event is malformed because it might mean the sdk spec +// has changed and rosetta needs to reflect those changes too +func eventToBalanceOperations(status string, event abci.Event) (operations []*rosettatypes.Operation, isBalanceEvent bool) { + + var ( + accountIdentifier string + coinChange sdk.Coins + isSub bool + ) + + switch event.Type { + default: + return nil, false + case banktypes.EventTypeCoinSpent: + spender, err := sdk.AccAddressFromBech32((string)(event.Attributes[0].Value)) + if err != nil { + panic(err) + } + coins, err := sdk.ParseCoinsNormalized((string)(event.Attributes[1].Value)) + if err != nil { + panic(err) + } + + isSub = true + coinChange = coins + accountIdentifier = spender.String() + + case banktypes.EventTypeCoinReceived: + receiver, err := sdk.AccAddressFromBech32((string)(event.Attributes[0].Value)) + if err != nil { + panic(err) + } + coins, err := sdk.ParseCoinsNormalized((string)(event.Attributes[1].Value)) + if err != nil { + panic(err) + } + + isSub = false + coinChange = coins + accountIdentifier = receiver.String() + } + + operations = make([]*rosettatypes.Operation, len(coinChange)) + + for i, coin := range coinChange { + + value := coin.Amount.String() + // in case the event is a subtract balance one the rewrite value with + // the negative coin identifier + if isSub { + value = "-" + value + } + + op := &rosettatypes.Operation{ + Type: event.Type, + Status: status, + Account: &rosettatypes.AccountIdentifier{Address: accountIdentifier}, + Amount: &rosettatypes.Amount{ + Value: value, + Currency: &rosettatypes.Currency{ + Symbol: coin.Denom, + Decimals: 0, + }, + }, + } + + operations[i] = op + } + return operations, true +} + +func toRosTx(txDecoder sdk.TxDecoder, rawTx tmtypes.Tx, txResult *abci.ResponseDeliverTx) (*rosettatypes.Transaction, error) { + // decode tx + tx, err := txDecoder(rawTx) + if err != nil { + return nil, crgerrs.WrapError(crgerrs.ErrCodec, err.Error()) + } + // get initial status, as per sdk design, if one msg fails + // the whole TX will be considered failing, so we can't have + // 1 msg being success and 1 msg being reverted + status := StatusSuccess + if txResult.Code != abci.CodeTypeOK { + status = StatusReverted + } + // get operations from msgs + msgs := tx.GetMsgs() + var rawTxOps []*rosettatypes.Operation + for _, msg := range msgs { + ops, err := opsFromMsg(status, msg) + if err != nil { + return nil, err + } + rawTxOps = append(rawTxOps, ops...) + } + + // now get balance events from response deliver tx + balanceOps := eventsToBalanceOperations(status, txResult.Events) + + // now normalize indexes + totalOps := normalizeOperationIndexes(rawTxOps, balanceOps) + + return &rosettatypes.Transaction{ + TransactionIdentifier: &rosettatypes.TransactionIdentifier{Hash: fmt.Sprintf("%x", rawTx.Hash())}, + Operations: totalOps, + }, nil +} + +// opsFromMsg will create an operation for each msg signer +// with the message proto name as type, and the raw fields +// as metadata +func opsFromMsg(status string, msg sdk.Msg) ([]*rosettatypes.Operation, error) { + opName := proto.MessageName(msg) + // in case proto does not recognize the message name + // then we should try to cast it to service msg, to + // check if it was wrapped or not, in case the cast + // from sdk.ServiceMsg to sdk.Msg fails, then a + // codec error is returned + if opName == "" { + unwrappedMsg, ok := msg.(sdk.ServiceMsg) + if !ok { + return nil, crgerrs.WrapError(crgerrs.ErrCodec, fmt.Sprintf("unrecognized message type: %T", msg)) + } + + msg, ok = unwrappedMsg.Request.(sdk.Msg) + if !ok { + return nil, crgerrs.WrapError( + crgerrs.ErrCodec, + fmt.Sprintf("unable to cast %T to sdk.Msg, method: %s", unwrappedMsg, unwrappedMsg.MethodName), + ) + } + + opName = proto.MessageName(msg) + if opName == "" { + return nil, crgerrs.WrapError(crgerrs.ErrCodec, fmt.Sprintf("unrecognized message type: %T", msg)) + } + } + + meta, err := msgToMetadata(msg) + if err != nil { + return nil, err + } + + ops := make([]*rosettatypes.Operation, len(msg.GetSigners())) + for i, signer := range msg.GetSigners() { + op := &rosettatypes.Operation{ + Type: opName, + Status: status, + Account: &rosettatypes.AccountIdentifier{Address: signer.String()}, + Metadata: meta, + } + + ops[i] = op + } + + return ops, nil +} + +// msgToMetadata converts an sdk.Msg to map[string]interface{} +func msgToMetadata(msg sdk.Msg) (map[string]interface{}, error) { + b, err := json.Marshal(msg) + if err != nil { + return nil, crgerrs.WrapError(crgerrs.ErrCodec, err.Error()) + } + m := make(map[string]interface{}) + err = json.Unmarshal(b, &m) + if err != nil { + return nil, crgerrs.WrapError(crgerrs.ErrCodec, err.Error()) + } + return m, nil +} + +// normalizeOperationIndexes adds the indexes to operations adhering to specific rules: +// operations related to messages will be always before than the balance ones +func normalizeOperationIndexes(msgOps []*rosettatypes.Operation, balanceOps []*rosettatypes.Operation) (finalOps []*rosettatypes.Operation) { + lenMsgOps := len(msgOps) + lenBalanceOps := len(balanceOps) + finalOps = make([]*rosettatypes.Operation, 0, lenMsgOps+lenBalanceOps) + + var currentIndex int64 + // add indexes to msg ops + for _, op := range msgOps { + op.OperationIdentifier = &rosettatypes.OperationIdentifier{ + Index: currentIndex, + } + + finalOps = append(finalOps, op) + currentIndex++ + } + + // add indexes to balance ops + for _, op := range balanceOps { + op.OperationIdentifier = &rosettatypes.OperationIdentifier{ + Index: currentIndex, + } + + finalOps = append(finalOps, op) + currentIndex++ + } + + return finalOps +} diff --git a/server/rosetta/client_v2_test.go b/server/rosetta/client_v2_test.go new file mode 100644 index 000000000000..6eaebbdfade8 --- /dev/null +++ b/server/rosetta/client_v2_test.go @@ -0,0 +1,47 @@ +package rosetta + +import ( + "context" + "testing" +) + +func TestClientV2(t *testing.T) { + cdc, ir := MakeCodec() + c, err := NewClient(&Config{ + Blockchain: "", + Network: "", + TendermintRPC: "tcp://localhost:26657", + GRPCEndpoint: "localhost:9090", + Addr: "", + Retries: 0, + Offline: false, + Codec: cdc, + InterfaceRegistry: ir, + }) + if err != nil { + t.Fatal(err) + } + + if err := c.Bootstrap(); err != nil { + t.Fatal(err) + } + + var h int64 = 3 + + txs, err := c.blockTxs(context.TODO(), &h) + if err != nil { + t.Fatal(err) + } + + for _, tx := range txs { + t.Logf("hash: %s", tx.TransactionIdentifier.Hash) + for _, op := range tx.Operations { + t.Logf("\t name: %s", op.Type) + t.Logf("\t\t index: %d", op.OperationIdentifier.Index) + if op.Amount != nil { + t.Logf("\t\t coin change: %s%s", op.Amount.Value, op.Amount.Currency.Symbol) + } + t.Logf("\t\t meta: %#v", op.Metadata) + } + } +} From 8f711a1d45b4bdae56516d0d3ddad560f9baae39 Mon Sep 17 00:00:00 2001 From: Frojdi Dymylja Date: Mon, 1 Mar 2021 16:12:18 +0100 Subject: [PATCH 02/33] change(rosetta): finalize data API --- server/rosetta/client_offline.go | 39 ++- server/rosetta/client_online.go | 161 ++++-------- server/rosetta/client_v2.go | 304 +++++----------------- server/rosetta/client_v2_test.go | 5 +- server/rosetta/conv_from_rosetta.go | 105 +------- server/rosetta/conv_to_rosetta.go | 375 +++++++++++++++++++++++++++- server/rosetta/types.go | 9 +- server/rosetta/util.go | 42 +--- x/distribution/types/msg.go | 4 +- x/staking/types/msg.go | 12 +- 10 files changed, 538 insertions(+), 518 deletions(-) diff --git a/server/rosetta/client_offline.go b/server/rosetta/client_offline.go index f619bfc6d2cf..dd1dc175f241 100644 --- a/server/rosetta/client_offline.go +++ b/server/rosetta/client_offline.go @@ -5,6 +5,8 @@ import ( "encoding/hex" "strings" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + "github.com/btcsuite/btcd/btcec" "github.com/coinbase/rosetta-sdk-go/types" crgerrs "github.com/tendermint/cosmos-rosetta-gateway/errors" @@ -20,11 +22,15 @@ import ( func (c *Client) OperationStatuses() []*types.OperationStatus { return []*types.OperationStatus{ { - Status: StatusSuccess, + Status: StatusTxSuccess, Successful: true, }, { - Status: StatusReverted, + Status: StatusTxReverted, + Successful: false, + }, + { + Status: StatusTxUnconfirmed, Successful: false, }, } @@ -37,17 +43,20 @@ func (c *Client) Version() string { func (c *Client) SupportedOperations() []string { var supportedOperations []string for _, ii := range c.ir.ListImplementations("cosmos.base.v1beta1.Msg") { - resolve, err := c.ir.Resolve(ii) + resolvedMsg, err := c.ir.Resolve(ii) if err != nil { continue } - if _, ok := resolve.(Msg); ok { + if _, ok := resolvedMsg.(Msg); ok { supportedOperations = append(supportedOperations, strings.TrimLeft(ii, "/")) } } - supportedOperations = append(supportedOperations, OperationFee) + supportedOperations = append( + supportedOperations, + OperationFee, banktypes.EventTypeCoinSpent, banktypes.EventTypeCoinReceived, + ) return supportedOperations } @@ -219,3 +228,23 @@ func (c *Client) PreprocessOperationsToOptions(_ context.Context, req *types.Con OptionGas: gas, }, nil } + +func (c *Client) AccountIdentifierFromPublicKey(pubKey *types.PublicKey) (*types.AccountIdentifier, error) { + if pubKey.CurveType != "secp256k1" { + return nil, crgerrs.WrapError(crgerrs.ErrUnsupportedCurve, "only secp256k1 supported") + } + + cmp, err := btcec.ParsePubKey(pubKey.Bytes, btcec.S256()) + if err != nil { + return nil, crgerrs.WrapError(crgerrs.ErrBadArgument, err.Error()) + } + + compressedPublicKey := make([]byte, secp256k1.PubKeySize) + copy(compressedPublicKey, cmp.SerializeCompressed()) + + pk := secp256k1.PubKey{Key: compressedPublicKey} + + return &types.AccountIdentifier{ + Address: sdk.AccAddress(pk.Address()).String(), + }, nil +} diff --git a/server/rosetta/client_online.go b/server/rosetta/client_online.go index 110430bb1806..9adf61374e0e 100644 --- a/server/rosetta/client_online.go +++ b/server/rosetta/client_online.go @@ -3,6 +3,7 @@ package rosetta import ( "bytes" "context" + "crypto/sha256" "encoding/hex" "fmt" "strconv" @@ -12,15 +13,10 @@ import ( abcitypes "github.com/tendermint/tendermint/abci/types" - "github.com/tendermint/btcd/btcec" - - "github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1" - "github.com/coinbase/rosetta-sdk-go/types" "google.golang.org/grpc/metadata" "github.com/tendermint/tendermint/rpc/client/http" - tmtypes "github.com/tendermint/tendermint/rpc/core/types" "google.golang.org/grpc" crgerrs "github.com/tendermint/cosmos-rosetta-gateway/errors" @@ -31,7 +27,6 @@ import ( codectypes "github.com/cosmos/cosmos-sdk/codec/types" sdk "github.com/cosmos/cosmos-sdk/types" grpctypes "github.com/cosmos/cosmos-sdk/types/grpc" - authclient "github.com/cosmos/cosmos-sdk/x/auth/client" authtx "github.com/cosmos/cosmos-sdk/x/auth/tx" auth "github.com/cosmos/cosmos-sdk/x/auth/types" bank "github.com/cosmos/cosmos-sdk/x/bank/types" @@ -54,27 +49,8 @@ type Client struct { clientCtx client.Context - version string -} - -func (c *Client) AccountIdentifierFromPublicKey(pubKey *types.PublicKey) (*types.AccountIdentifier, error) { - if pubKey.CurveType != "secp256k1" { - return nil, crgerrs.WrapError(crgerrs.ErrUnsupportedCurve, "only secp256k1 supported") - } - - cmp, err := btcec.ParsePubKey(pubKey.Bytes, btcec.S256()) - if err != nil { - return nil, crgerrs.WrapError(crgerrs.ErrBadArgument, err.Error()) - } - - compressedPublicKey := make([]byte, secp256k1.PubKeySize) - copy(compressedPublicKey, cmp.SerializeCompressed()) - - pk := secp256k1.PubKey{Key: compressedPublicKey} - - return &types.AccountIdentifier{ - Address: sdk.AccAddress(pk.Address()).String(), - }, nil + txDecoder sdk.TxDecoder + version string } // NewClient instantiates a new online servicer @@ -87,9 +63,10 @@ func NewClient(cfg *Config) (*Client, error) { } return &Client{ - config: cfg, - ir: cfg.InterfaceRegistry, - version: fmt.Sprintf("%s/%s", info.AppName, v), + config: cfg, + ir: cfg.InterfaceRegistry, + version: fmt.Sprintf("%s/%s", info.AppName, v), + txDecoder: authtx.NewTxConfig(cfg.Codec, authtx.DefaultSignModes).TxDecoder(), }, nil } @@ -147,7 +124,7 @@ func (c *Client) BlockByHash(ctx context.Context, hash string) (crgtypes.BlockRe return crgtypes.BlockResponse{}, err } - return buildBlockResponse(block), nil + return tmResultBlockToRosettaBlockResponse(block), nil } func (c *Client) BlockByHeight(ctx context.Context, height *int64) (crgtypes.BlockResponse, error) { @@ -156,50 +133,26 @@ func (c *Client) BlockByHeight(ctx context.Context, height *int64) (crgtypes.Blo return crgtypes.BlockResponse{}, err } - return buildBlockResponse(block), nil -} - -func buildBlockResponse(block *tmtypes.ResultBlock) crgtypes.BlockResponse { - return crgtypes.BlockResponse{ - Block: TMBlockToRosettaBlockIdentifier(block), - ParentBlock: TMBlockToRosettaParentBlockIdentifier(block), - MillisecondTimestamp: timeToMilliseconds(block.Block.Time), - TxCount: int64(len(block.Block.Txs)), - } + return tmResultBlockToRosettaBlockResponse(block), nil } func (c *Client) BlockTransactionsByHash(ctx context.Context, hash string) (crgtypes.BlockTransactionsResponse, error) { + // TODO(fdymylja): use a faster path, by searching the block by hash, instead of doing a double query operation blockResp, err := c.BlockByHash(ctx, hash) if err != nil { return crgtypes.BlockTransactionsResponse{}, err } - txs, err := c.listTransactionsInBlock(ctx, blockResp.Block.Index) - if err != nil { - return crgtypes.BlockTransactionsResponse{}, err - } - - return crgtypes.BlockTransactionsResponse{ - BlockResponse: blockResp, - Transactions: sdkTxsWithHashToRosettaTxs(txs), - }, nil + return c.blockTxs(ctx, &blockResp.Block.Index) } func (c *Client) BlockTransactionsByHeight(ctx context.Context, height *int64) (crgtypes.BlockTransactionsResponse, error) { - blockResp, err := c.BlockByHeight(ctx, height) + blockTxResp, err := c.blockTxs(ctx, height) if err != nil { return crgtypes.BlockTransactionsResponse{}, err } - txs, err := c.listTransactionsInBlock(ctx, blockResp.Block.Index) - if err != nil { - return crgtypes.BlockTransactionsResponse{}, err - } - - return crgtypes.BlockTransactionsResponse{ - BlockResponse: blockResp, - Transactions: sdkTxsWithHashToRosettaTxs(txs), - }, nil + return blockTxResp, nil } // Coins fetches the existing coins in the application @@ -211,24 +164,9 @@ func (c *Client) coins(ctx context.Context) (sdk.Coins, error) { return supply.Supply, nil } -// listTransactionsInBlock returns the list of the transactions in a block given its height -func (c *Client) listTransactionsInBlock(ctx context.Context, height int64) ([]*sdkTxWithHash, error) { - txQuery := fmt.Sprintf(`tx.height=%d`, height) - txList, err := c.clientCtx.Client.TxSearch(ctx, txQuery, true, nil, nil, "") - if err != nil { - return nil, crgerrs.WrapError(crgerrs.ErrUnknown, err.Error()) - } - - sdkTxs, err := tmResultTxsToSdkTxsWithHash(c.clientCtx.TxConfig.TxDecoder(), txList.Txs) - if err != nil { - return nil, err - } - return sdkTxs, nil -} - func (c *Client) TxOperationsAndSignersAccountIdentifiers(signed bool, txBytes []byte) (ops []*types.Operation, signers []*types.AccountIdentifier, err error) { txConfig := c.getTxConfig() - rawTx, err := txConfig.TxDecoder()(txBytes) + rawTx, err := c.txDecoder(txBytes) if err != nil { return nil, nil, err } @@ -253,22 +191,30 @@ func (c *Client) TxOperationsAndSignersAccountIdentifiers(signed bool, txBytes [ } // GetTx returns a transaction given its hash -func (c *Client) GetTx(_ context.Context, hash string) (*types.Transaction, error) { - txResp, err := authclient.QueryTx(c.clientCtx, hash) +func (c *Client) GetTx(ctx context.Context, hash string) (*types.Transaction, error) { + hashBytes, err := hex.DecodeString(hash) if err != nil { - return nil, crgerrs.WrapError(crgerrs.ErrUnknown, err.Error()) - } - var sdkTx sdk.Tx - err = c.ir.UnpackAny(txResp.Tx, &sdkTx) - if err != nil { - return nil, crgerrs.WrapError(crgerrs.ErrCodec, err.Error()) + return nil, crgerrs.WrapError(crgerrs.ErrCodec, fmt.Sprintf("bad tx hash: %s", err)) + } + + // here we check for the hash length to understand if it is a begin or endblock tx or a standard tendermint tx + switch len(hashBytes) { + case beginEndBlockTxSize: + // verify if it's end or begin block operations we're trying to query + switch hashBytes[0] { + case beginBlockHashStart: + return c.beginBlockTx(ctx, hashBytes[1:]) + case endBlockHashStart: + return c.endBlockTx(ctx, hashBytes[1:]) + default: + return nil, crgerrs.WrapError(crgerrs.ErrBadArgument, fmt.Sprintf("bad begin endblock starting byte: %x", hashBytes[0])) + } + // standard tx... + case sha256.Size: + return c.getTx(ctx, hashBytes) + default: + return nil, crgerrs.WrapError(crgerrs.ErrBadArgument, fmt.Sprintf("invalid tx size: %d", len(hashBytes))) } - return sdkTxWithHashToOperations(&sdkTxWithHash{ - HexHash: txResp.TxHash, - Code: txResp.Code, - Log: txResp.RawLog, - Tx: sdkTx, - }), nil } // GetUnconfirmedTx gets an unconfirmed transaction given its hash @@ -283,22 +229,25 @@ func (c *Client) GetUnconfirmedTx(ctx context.Context, hash string) (*types.Tran return nil, crgerrs.WrapError(crgerrs.ErrInterpreting, "invalid hash") } - for _, tx := range res.Txs { - if bytes.Equal(tx.Hash(), hashAsBytes) { - sdkTx, err := tmTxToSdkTx(c.clientCtx.TxConfig.TxDecoder(), tx) - if err != nil { - return nil, err - } + // assert that correct tx length is provided + switch len(hashAsBytes) { + default: + return nil, crgerrs.WrapError(crgerrs.ErrBadArgument, fmt.Sprintf("unrecognized tx size: %d", len(hashAsBytes))) + case beginEndBlockTxSize: + return nil, crgerrs.WrapError(crgerrs.ErrBadArgument, fmt.Sprintf("endblock and begin block txs cannot be unconfirmed")) + case deliverTxSize: + break + } - return &types.Transaction{ - TransactionIdentifier: TmTxToRosettaTxsIdentifier(tx), - Operations: sdkTxToOperations(sdkTx, false, false), - Metadata: nil, - }, nil + // iterate over unconfirmed txs to find the one with matching hash + for _, unconfirmedTx := range res.Txs { + if !bytes.Equal(unconfirmedTx.Hash(), hashAsBytes) { + continue } - } - return nil, crgerrs.WrapError(crgerrs.ErrNotFound, "transaction not found in mempool") + return sdkTxToRosettaTx(c.txDecoder, unconfirmedTx, nil) + } + return nil, crgerrs.WrapError(crgerrs.ErrNotFound, "transaction not found in mempool: "+hash) } // Mempool returns the unconfirmed transactions in the mempool @@ -308,7 +257,7 @@ func (c *Client) Mempool(ctx context.Context) ([]*types.TransactionIdentifier, e return nil, err } - return TMTxsToRosettaTxsIdentifiers(txs.Txs), nil + return tmTxsToRosettaTxsIdentifiers(txs.Txs), nil } // Peers gets the number of peers @@ -317,7 +266,7 @@ func (c *Client) Peers(ctx context.Context) ([]*types.Peer, error) { if err != nil { return nil, crgerrs.WrapError(crgerrs.ErrUnknown, err.Error()) } - return TmPeersToRosettaPeers(netInfo.Peers), nil + return tmPeersToRosettaPeers(netInfo.Peers), nil } func (c *Client) Status(ctx context.Context) (*types.SyncStatus, error) { @@ -325,7 +274,7 @@ func (c *Client) Status(ctx context.Context) (*types.SyncStatus, error) { if err != nil { return nil, crgerrs.WrapError(crgerrs.ErrUnknown, err.Error()) } - return TMStatusToRosettaSyncStatus(status), err + return tmStatusToRosettaSyncStatus(status), err } func (c *Client) getTxConfig() client.TxConfig { diff --git a/server/rosetta/client_v2.go b/server/rosetta/client_v2.go index b36cf8ef1402..522dc6029f6e 100644 --- a/server/rosetta/client_v2.go +++ b/server/rosetta/client_v2.go @@ -3,36 +3,73 @@ package rosetta import ( "context" "crypto/sha256" - "encoding/json" - "fmt" - rosettatypes "github.com/coinbase/rosetta-sdk-go/types" + crgtypes "github.com/tendermint/cosmos-rosetta-gateway/types" - sdk "github.com/cosmos/cosmos-sdk/types" + rosettatypes "github.com/coinbase/rosetta-sdk-go/types" - "github.com/gogo/protobuf/proto" crgerrs "github.com/tendermint/cosmos-rosetta-gateway/errors" - abci "github.com/tendermint/tendermint/abci/types" - tmtypes "github.com/tendermint/tendermint/types" +) - banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" +const ( + deliverTxSize = sha256.Size + beginEndBlockTxSize = deliverTxSize + 1 + endBlockHashStart = 0x0 + beginBlockHashStart = 0x1 + burnerAddressIdentifier = "burner" ) -const mockBeginEndBlockTxLength = sha256.Size + 1 -const endBlockHashStart = 0x0 -const beginBlockHashStart = 0x1 -const burnerAddressIdentifier = "burner" +func (c *Client) getTx(ctx context.Context, txHash []byte) (*rosettatypes.Transaction, error) { + rawTx, err := c.clientCtx.Client.Tx(ctx, txHash, true) + if err != nil { + return nil, crgerrs.WrapError(crgerrs.ErrUnknown, err.Error()) + } + return sdkTxToRosettaTx(c.clientCtx.TxConfig.TxDecoder(), rawTx.Tx, &rawTx.TxResult) +} + +func (c *Client) beginBlockTx(ctx context.Context, blockHash []byte) (*rosettatypes.Transaction, error) { + // get block height by hash + block, err := c.clientCtx.Client.BlockByHash(ctx, blockHash) + if err != nil { + return nil, crgerrs.WrapError(crgerrs.ErrUnknown, err.Error()) + } -func (c *Client) blockTxs(ctx context.Context, height *int64) ([]*rosettatypes.Transaction, error) { + // get block txs + fullBlock, err := c.blockTxs(ctx, &block.Block.Height) + if err != nil { + return nil, err + } + + return fullBlock.Transactions[0], nil +} + +func (c *Client) endBlockTx(ctx context.Context, blockHash []byte) (*rosettatypes.Transaction, error) { + // get block height by hash + block, err := c.clientCtx.Client.BlockByHash(ctx, blockHash) + if err != nil { + return nil, crgerrs.WrapError(crgerrs.ErrUnknown, err.Error()) + } + + // get block txs + fullBlock, err := c.blockTxs(ctx, &block.Block.Height) + if err != nil { + return nil, err + } + + // get last tx + return fullBlock.Transactions[len(fullBlock.Transactions)-1], nil +} + +func (c *Client) blockTxs(ctx context.Context, height *int64) (crgtypes.BlockTransactionsResponse, error) { // get block info blockInfo, err := c.clientCtx.Client.Block(ctx, height) if err != nil { - return nil, err + return crgtypes.BlockTransactionsResponse{}, err } // get block events blockResults, err := c.clientCtx.Client.BlockResults(ctx, height) if err != nil { - return nil, err + return crgtypes.BlockTransactionsResponse{}, err } if len(blockResults.TxsResults) != len(blockInfo.Block.Txs) { @@ -44,23 +81,24 @@ func (c *Client) blockTxs(ctx context.Context, height *int64) ([]*rosettatypes.T TransactionIdentifier: &rosettatypes.TransactionIdentifier{Hash: beginBlockTxHash(blockInfo.BlockID.Hash)}, Operations: normalizeOperationIndexes( nil, - eventsToBalanceOperations(StatusSuccess, blockResults.BeginBlockEvents), + sdkEventsToBalanceOperations(StatusTxSuccess, blockResults.BeginBlockEvents), ), } + endBlockTx := &rosettatypes.Transaction{ TransactionIdentifier: &rosettatypes.TransactionIdentifier{Hash: endBlockTxHash(blockInfo.BlockID.Hash)}, Operations: normalizeOperationIndexes( nil, - eventsToBalanceOperations(StatusSuccess, blockResults.EndBlockEvents), + sdkEventsToBalanceOperations(StatusTxSuccess, blockResults.EndBlockEvents), ), } deliverTx := make([]*rosettatypes.Transaction, len(blockInfo.Block.Txs)) // process normal txs for i, tx := range blockInfo.Block.Txs { - rosTx, err := toRosTx(c.clientCtx.TxConfig.TxDecoder(), tx, blockResults.TxsResults[i]) + rosTx, err := sdkTxToRosettaTx(c.clientCtx.TxConfig.TxDecoder(), tx, blockResults.TxsResults[i]) if err != nil { - return nil, err + return crgtypes.BlockTransactionsResponse{}, err } deliverTx[i] = rosTx } @@ -70,230 +108,8 @@ func (c *Client) blockTxs(ctx context.Context, height *int64) ([]*rosettatypes.T finalTxs = append(finalTxs, deliverTx...) finalTxs = append(finalTxs, endBlockTx) - return finalTxs, nil -} - -func endBlockTxHash(hash []byte) string { - return fmt.Sprintf("%x%x", endBlockHashStart, hash) -} - -func beginBlockTxHash(hash []byte) string { - return fmt.Sprintf("%x%x", beginBlockHashStart, hash) -} - -func eventsToBalanceOperations(status string, events []abci.Event) []*rosettatypes.Operation { - var ops []*rosettatypes.Operation - - for _, e := range events { - balanceOps, ok := eventToBalanceOperations(status, e) - if !ok { - continue - } - ops = append(ops, balanceOps...) - } - - return ops -} - -// eventToBalanceOperations converts an event to a rosetta balance operation -// it will panic if the event is malformed because it might mean the sdk spec -// has changed and rosetta needs to reflect those changes too -func eventToBalanceOperations(status string, event abci.Event) (operations []*rosettatypes.Operation, isBalanceEvent bool) { - - var ( - accountIdentifier string - coinChange sdk.Coins - isSub bool - ) - - switch event.Type { - default: - return nil, false - case banktypes.EventTypeCoinSpent: - spender, err := sdk.AccAddressFromBech32((string)(event.Attributes[0].Value)) - if err != nil { - panic(err) - } - coins, err := sdk.ParseCoinsNormalized((string)(event.Attributes[1].Value)) - if err != nil { - panic(err) - } - - isSub = true - coinChange = coins - accountIdentifier = spender.String() - - case banktypes.EventTypeCoinReceived: - receiver, err := sdk.AccAddressFromBech32((string)(event.Attributes[0].Value)) - if err != nil { - panic(err) - } - coins, err := sdk.ParseCoinsNormalized((string)(event.Attributes[1].Value)) - if err != nil { - panic(err) - } - - isSub = false - coinChange = coins - accountIdentifier = receiver.String() - } - - operations = make([]*rosettatypes.Operation, len(coinChange)) - - for i, coin := range coinChange { - - value := coin.Amount.String() - // in case the event is a subtract balance one the rewrite value with - // the negative coin identifier - if isSub { - value = "-" + value - } - - op := &rosettatypes.Operation{ - Type: event.Type, - Status: status, - Account: &rosettatypes.AccountIdentifier{Address: accountIdentifier}, - Amount: &rosettatypes.Amount{ - Value: value, - Currency: &rosettatypes.Currency{ - Symbol: coin.Denom, - Decimals: 0, - }, - }, - } - - operations[i] = op - } - return operations, true -} - -func toRosTx(txDecoder sdk.TxDecoder, rawTx tmtypes.Tx, txResult *abci.ResponseDeliverTx) (*rosettatypes.Transaction, error) { - // decode tx - tx, err := txDecoder(rawTx) - if err != nil { - return nil, crgerrs.WrapError(crgerrs.ErrCodec, err.Error()) - } - // get initial status, as per sdk design, if one msg fails - // the whole TX will be considered failing, so we can't have - // 1 msg being success and 1 msg being reverted - status := StatusSuccess - if txResult.Code != abci.CodeTypeOK { - status = StatusReverted - } - // get operations from msgs - msgs := tx.GetMsgs() - var rawTxOps []*rosettatypes.Operation - for _, msg := range msgs { - ops, err := opsFromMsg(status, msg) - if err != nil { - return nil, err - } - rawTxOps = append(rawTxOps, ops...) - } - - // now get balance events from response deliver tx - balanceOps := eventsToBalanceOperations(status, txResult.Events) - - // now normalize indexes - totalOps := normalizeOperationIndexes(rawTxOps, balanceOps) - - return &rosettatypes.Transaction{ - TransactionIdentifier: &rosettatypes.TransactionIdentifier{Hash: fmt.Sprintf("%x", rawTx.Hash())}, - Operations: totalOps, + return crgtypes.BlockTransactionsResponse{ + BlockResponse: tmResultBlockToRosettaBlockResponse(blockInfo), + Transactions: finalTxs, }, nil } - -// opsFromMsg will create an operation for each msg signer -// with the message proto name as type, and the raw fields -// as metadata -func opsFromMsg(status string, msg sdk.Msg) ([]*rosettatypes.Operation, error) { - opName := proto.MessageName(msg) - // in case proto does not recognize the message name - // then we should try to cast it to service msg, to - // check if it was wrapped or not, in case the cast - // from sdk.ServiceMsg to sdk.Msg fails, then a - // codec error is returned - if opName == "" { - unwrappedMsg, ok := msg.(sdk.ServiceMsg) - if !ok { - return nil, crgerrs.WrapError(crgerrs.ErrCodec, fmt.Sprintf("unrecognized message type: %T", msg)) - } - - msg, ok = unwrappedMsg.Request.(sdk.Msg) - if !ok { - return nil, crgerrs.WrapError( - crgerrs.ErrCodec, - fmt.Sprintf("unable to cast %T to sdk.Msg, method: %s", unwrappedMsg, unwrappedMsg.MethodName), - ) - } - - opName = proto.MessageName(msg) - if opName == "" { - return nil, crgerrs.WrapError(crgerrs.ErrCodec, fmt.Sprintf("unrecognized message type: %T", msg)) - } - } - - meta, err := msgToMetadata(msg) - if err != nil { - return nil, err - } - - ops := make([]*rosettatypes.Operation, len(msg.GetSigners())) - for i, signer := range msg.GetSigners() { - op := &rosettatypes.Operation{ - Type: opName, - Status: status, - Account: &rosettatypes.AccountIdentifier{Address: signer.String()}, - Metadata: meta, - } - - ops[i] = op - } - - return ops, nil -} - -// msgToMetadata converts an sdk.Msg to map[string]interface{} -func msgToMetadata(msg sdk.Msg) (map[string]interface{}, error) { - b, err := json.Marshal(msg) - if err != nil { - return nil, crgerrs.WrapError(crgerrs.ErrCodec, err.Error()) - } - m := make(map[string]interface{}) - err = json.Unmarshal(b, &m) - if err != nil { - return nil, crgerrs.WrapError(crgerrs.ErrCodec, err.Error()) - } - return m, nil -} - -// normalizeOperationIndexes adds the indexes to operations adhering to specific rules: -// operations related to messages will be always before than the balance ones -func normalizeOperationIndexes(msgOps []*rosettatypes.Operation, balanceOps []*rosettatypes.Operation) (finalOps []*rosettatypes.Operation) { - lenMsgOps := len(msgOps) - lenBalanceOps := len(balanceOps) - finalOps = make([]*rosettatypes.Operation, 0, lenMsgOps+lenBalanceOps) - - var currentIndex int64 - // add indexes to msg ops - for _, op := range msgOps { - op.OperationIdentifier = &rosettatypes.OperationIdentifier{ - Index: currentIndex, - } - - finalOps = append(finalOps, op) - currentIndex++ - } - - // add indexes to balance ops - for _, op := range balanceOps { - op.OperationIdentifier = &rosettatypes.OperationIdentifier{ - Index: currentIndex, - } - - finalOps = append(finalOps, op) - currentIndex++ - } - - return finalOps -} diff --git a/server/rosetta/client_v2_test.go b/server/rosetta/client_v2_test.go index 6eaebbdfade8..477ff1a236b2 100644 --- a/server/rosetta/client_v2_test.go +++ b/server/rosetta/client_v2_test.go @@ -28,12 +28,12 @@ func TestClientV2(t *testing.T) { var h int64 = 3 - txs, err := c.blockTxs(context.TODO(), &h) + blockTransactionsResponse, err := c.blockTxs(context.TODO(), &h) if err != nil { t.Fatal(err) } - for _, tx := range txs { + for _, tx := range blockTransactionsResponse.Transactions { t.Logf("hash: %s", tx.TransactionIdentifier.Hash) for _, op := range tx.Operations { t.Logf("\t name: %s", op.Type) @@ -41,6 +41,7 @@ func TestClientV2(t *testing.T) { if op.Amount != nil { t.Logf("\t\t coin change: %s%s", op.Amount.Value, op.Amount.Currency.Symbol) } + t.Logf("\t\t address: %s", op.Account.Address) t.Logf("\t\t meta: %#v", op.Metadata) } } diff --git a/server/rosetta/conv_from_rosetta.go b/server/rosetta/conv_from_rosetta.go index da9ea5b2ed6f..0c7f851ee8fe 100644 --- a/server/rosetta/conv_from_rosetta.go +++ b/server/rosetta/conv_from_rosetta.go @@ -1,25 +1,12 @@ package rosetta import ( - "fmt" - "time" - "github.com/coinbase/rosetta-sdk-go/types" - tmcoretypes "github.com/tendermint/tendermint/rpc/core/types" - tmtypes "github.com/tendermint/tendermint/types" sdk "github.com/cosmos/cosmos-sdk/types" ) -// timeToMilliseconds converts time to milliseconds timestamp -func timeToMilliseconds(t time.Time) int64 { - return t.UnixNano() / (int64(time.Millisecond) / int64(time.Nanosecond)) -} - // sdkCoinsToRosettaAmounts converts []sdk.Coin to rosetta amounts -// availableCoins keeps track of current available coins vs the coins -// owned by an address. This is required to support historical balances -// as rosetta expects them to be set to 0, if an address does not own them func sdkCoinsToRosettaAmounts(ownedCoins []sdk.Coin, availableCoins sdk.Coins) []*types.Amount { amounts := make([]*types.Amount, len(availableCoins)) ownedCoinsMap := make(map[string]sdk.Int, len(availableCoins)) @@ -50,27 +37,6 @@ func sdkCoinsToRosettaAmounts(ownedCoins []sdk.Coin, availableCoins sdk.Coins) [ return amounts } -// sdkTxsWithHashToRosettaTxs converts sdk transactions wrapped with their hash to rosetta transactions -func sdkTxsWithHashToRosettaTxs(txs []*sdkTxWithHash) []*types.Transaction { - converted := make([]*types.Transaction, len(txs)) - for i, tx := range txs { - converted[i] = sdkTxWithHashToOperations(tx) - } - - return converted -} - -func sdkTxWithHashToOperations(tx *sdkTxWithHash) *types.Transaction { - hasError := tx.Code != 0 - return &types.Transaction{ - TransactionIdentifier: &types.TransactionIdentifier{Hash: tx.HexHash}, - Operations: sdkTxToOperations(tx.Tx, true, hasError), - Metadata: map[string]interface{}{ - Log: tx.Log, - }, - } -} - // sdkTxToOperations converts an sdk.Tx to rosetta operations func sdkTxToOperations(tx sdk.Tx, withStatus, hasError bool) []*types.Operation { var operations []*types.Operation @@ -102,7 +68,7 @@ func rosettaFeeOperationsFromCoins(coins sdk.Coins, account string, withStatus b feeOps := make([]*types.Operation, 0) var status string if withStatus { - status = StatusSuccess + status = StatusTxSuccess } for i, coin := range coins { @@ -140,72 +106,3 @@ func sdkMsgsToRosettaOperations(msgs []sdk.Msg, withStatus bool, hasError bool) return operations } - -// TMTxsToRosettaTxsIdentifiers converts a tendermint raw transactions into an array of rosetta tx identifiers -func TMTxsToRosettaTxsIdentifiers(txs []tmtypes.Tx) []*types.TransactionIdentifier { - converted := make([]*types.TransactionIdentifier, len(txs)) - for i, tx := range txs { - converted[i] = TmTxToRosettaTxsIdentifier(tx) - } - - return converted -} - -// TmTxToRosettaTxsIdentifier converts a tendermint raw transaction into a rosetta tx identifier -func TmTxToRosettaTxsIdentifier(tx tmtypes.Tx) *types.TransactionIdentifier { - return &types.TransactionIdentifier{Hash: fmt.Sprintf("%x", tx.Hash())} -} - -// TMBlockToRosettaBlockIdentifier converts a tendermint result block to a rosetta block identifier -func TMBlockToRosettaBlockIdentifier(block *tmcoretypes.ResultBlock) *types.BlockIdentifier { - return &types.BlockIdentifier{ - Index: block.Block.Height, - Hash: block.Block.Hash().String(), - } -} - -// TmPeersToRosettaPeers converts tendermint peers to rosetta ones -func TmPeersToRosettaPeers(peers []tmcoretypes.Peer) []*types.Peer { - converted := make([]*types.Peer, len(peers)) - - for i, peer := range peers { - converted[i] = &types.Peer{ - PeerID: peer.NodeInfo.Moniker, - Metadata: map[string]interface{}{ - "addr": peer.NodeInfo.ListenAddr, - }, - } - } - - return converted -} - -// TMStatusToRosettaSyncStatus converts a tendermint status to rosetta sync status -func TMStatusToRosettaSyncStatus(status *tmcoretypes.ResultStatus) *types.SyncStatus { - // determine sync status - var stage = StageSynced - if status.SyncInfo.CatchingUp { - stage = StageSyncing - } - - return &types.SyncStatus{ - CurrentIndex: status.SyncInfo.LatestBlockHeight, - TargetIndex: nil, // sync info does not allow us to get target height - Stage: &stage, - } -} - -// TMBlockToRosettaParentBlockIdentifier returns the parent block identifier from the last block -func TMBlockToRosettaParentBlockIdentifier(block *tmcoretypes.ResultBlock) *types.BlockIdentifier { - if block.Block.Height == 1 { - return &types.BlockIdentifier{ - Index: 1, - Hash: fmt.Sprintf("%X", block.BlockID.Hash.Bytes()), - } - } - - return &types.BlockIdentifier{ - Index: block.Block.Height - 1, - Hash: fmt.Sprintf("%X", block.Block.LastBlockID.Hash.Bytes()), - } -} diff --git a/server/rosetta/conv_to_rosetta.go b/server/rosetta/conv_to_rosetta.go index 09146eed4f3e..3a1dc004ee25 100644 --- a/server/rosetta/conv_to_rosetta.go +++ b/server/rosetta/conv_to_rosetta.go @@ -1,21 +1,250 @@ package rosetta import ( + "encoding/json" "fmt" "strconv" "strings" + "github.com/gogo/protobuf/proto" + crgerrs "github.com/tendermint/cosmos-rosetta-gateway/errors" + crgtypes "github.com/tendermint/cosmos-rosetta-gateway/types" + abci "github.com/tendermint/tendermint/abci/types" + tmcoretypes "github.com/tendermint/tendermint/rpc/core/types" + tmtypes "github.com/tendermint/tendermint/types" + + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + "github.com/gogo/protobuf/jsonpb" - "github.com/coinbase/rosetta-sdk-go/types" + rosettatypes "github.com/coinbase/rosetta-sdk-go/types" sdk "github.com/cosmos/cosmos-sdk/types" ) +// ----------------------- sdk to rosetta ------------------------ + +// sdkTxToRosettaTx converts a tendermint raw transaction and its result (if provided) to a rosetta transaction +// using the sdk transaction decoder +func sdkTxToRosettaTx(txDecoder sdk.TxDecoder, rawTx tmtypes.Tx, txResult *abci.ResponseDeliverTx) ( + *rosettatypes.Transaction, + error) { + // decode tx + tx, err := txDecoder(rawTx) + if err != nil { + return nil, crgerrs.WrapError(crgerrs.ErrCodec, err.Error()) + } + // get initial status, as per sdk design, if one msg fails + // the whole TX will be considered failing, so we can't have + // 1 msg being success and 1 msg being reverted + status := StatusTxSuccess + switch txResult { + // if nil, we're probably checking an unconfirmed tx + case nil: + status = StatusTxUnconfirmed + // set the status + default: + if txResult.Code != abci.CodeTypeOK { + status = StatusTxReverted + } + } + // get operations from msgs + msgs := tx.GetMsgs() + var rawTxOps []*rosettatypes.Operation + for _, msg := range msgs { + ops, err := sdkMsgToRosettaOperation(status, msg) + if err != nil { + return nil, err + } + rawTxOps = append(rawTxOps, ops...) + } + + // now get balance events from response deliver tx + var balanceOps []*rosettatypes.Operation + // tx result might be nil, in case we're querying an unconfirmed tx from the mempool + if txResult != nil { + balanceOps = sdkEventsToBalanceOperations(status, txResult.Events) + } + + // now normalize indexes + totalOps := normalizeOperationIndexes(rawTxOps, balanceOps) + + return &rosettatypes.Transaction{ + TransactionIdentifier: &rosettatypes.TransactionIdentifier{Hash: fmt.Sprintf("%x", rawTx.Hash())}, + Operations: totalOps, + }, nil +} + +// sdkEventsToBalanceOperations takes a slice of tendermint events and converts them to +// balance operations +func sdkEventsToBalanceOperations(status string, events []abci.Event) []*rosettatypes.Operation { + var ops []*rosettatypes.Operation + + for _, e := range events { + balanceOps, ok := sdkEventToBalanceOperations(status, e) + if !ok { + continue + } + ops = append(ops, balanceOps...) + } + + return ops +} + +// sdkEventToBalanceOperations converts an event to a rosetta balance operation +// it will panic if the event is malformed because it might mean the sdk spec +// has changed and rosetta needs to reflect those changes too. +// The balance operations are multiple, one for each denom. +func sdkEventToBalanceOperations(status string, event abci.Event) (operations []*rosettatypes.Operation, isBalanceEvent bool) { + + var ( + accountIdentifier string + coinChange sdk.Coins + isSub bool + ) + + switch event.Type { + default: + return nil, false + case banktypes.EventTypeCoinSpent: + spender, err := sdk.AccAddressFromBech32((string)(event.Attributes[0].Value)) + if err != nil { + panic(err) + } + coins, err := sdk.ParseCoinsNormalized((string)(event.Attributes[1].Value)) + if err != nil { + panic(err) + } + + isSub = true + coinChange = coins + accountIdentifier = spender.String() + + case banktypes.EventTypeCoinReceived: + receiver, err := sdk.AccAddressFromBech32((string)(event.Attributes[0].Value)) + if err != nil { + panic(err) + } + coins, err := sdk.ParseCoinsNormalized((string)(event.Attributes[1].Value)) + if err != nil { + panic(err) + } + + isSub = false + coinChange = coins + accountIdentifier = receiver.String() + + // rosetta does not have the concept of burning coins, so we need to mock + // the burn as a send to an address that cannot be resolved to anything + case banktypes.EventTypeCoinBurn: + coins, err := sdk.ParseCoinsNormalized((string)(event.Attributes[1].Value)) + if err != nil { + panic(err) + } + + coinChange = coins + accountIdentifier = burnerAddressIdentifier + } + + operations = make([]*rosettatypes.Operation, len(coinChange)) + + for i, coin := range coinChange { + + value := coin.Amount.String() + // in case the event is a subtract balance one the rewrite value with + // the negative coin identifier + if isSub { + value = "-" + value + } + + op := &rosettatypes.Operation{ + Type: event.Type, + Status: status, + Account: &rosettatypes.AccountIdentifier{Address: accountIdentifier}, + Amount: &rosettatypes.Amount{ + Value: value, + Currency: &rosettatypes.Currency{ + Symbol: coin.Denom, + Decimals: 0, + }, + }, + } + + operations[i] = op + } + return operations, true +} + +// sdkMsgToRosettaOperation will create an operation for each msg signer +// with the message proto name as type, and the raw fields +// as metadata +func sdkMsgToRosettaOperation(status string, msg sdk.Msg) ([]*rosettatypes.Operation, error) { + opName := proto.MessageName(msg) + // in case proto does not recognize the message name + // then we should try to cast it to service msg, to + // check if it was wrapped or not, in case the cast + // from sdk.ServiceMsg to sdk.Msg fails, then a + // codec error is returned + if opName == "" { + unwrappedMsg, ok := msg.(sdk.ServiceMsg) + if !ok { + return nil, crgerrs.WrapError(crgerrs.ErrCodec, fmt.Sprintf("unrecognized message type: %T", msg)) + } + + msg, ok = unwrappedMsg.Request.(sdk.Msg) + if !ok { + return nil, crgerrs.WrapError( + crgerrs.ErrCodec, + fmt.Sprintf("unable to cast %T to sdk.Msg, method: %s", unwrappedMsg, unwrappedMsg.MethodName), + ) + } + + opName = proto.MessageName(msg) + if opName == "" { + return nil, crgerrs.WrapError(crgerrs.ErrCodec, fmt.Sprintf("unrecognized message type: %T", msg)) + } + } + + meta, err := sdkMsgToRosettaMetadata(msg) + if err != nil { + return nil, err + } + + ops := make([]*rosettatypes.Operation, len(msg.GetSigners())) + for i, signer := range msg.GetSigners() { + op := &rosettatypes.Operation{ + Type: opName, + Status: status, + Account: &rosettatypes.AccountIdentifier{Address: signer.String()}, + Metadata: meta, + } + + ops[i] = op + } + + return ops, nil +} + +// sdkMsgToRosettaMetadata converts an sdk.Msg to map[string]interface{} +func sdkMsgToRosettaMetadata(msg sdk.Msg) (map[string]interface{}, error) { + b, err := json.Marshal(msg) + if err != nil { + return nil, crgerrs.WrapError(crgerrs.ErrCodec, err.Error()) + } + m := make(map[string]interface{}) + err = json.Unmarshal(b, &m) + if err != nil { + return nil, crgerrs.WrapError(crgerrs.ErrCodec, err.Error()) + } + return m, nil +} + +// ---------------------------- end ------------------------------ + // opsToMsgsAndFees converts rosetta operations to sdk.Msg and fees represented as sdk.Coins -func opsToMsgsAndFees(interfaceRegistry jsonpb.AnyResolver, ops []*types.Operation) ([]sdk.Msg, sdk.Coins, error) { - var feeAmnt []*types.Amount - var newOps []*types.Operation +func opsToMsgsAndFees(interfaceRegistry jsonpb.AnyResolver, ops []*rosettatypes.Operation) ([]sdk.Msg, sdk.Coins, error) { + var feeAmnt []*rosettatypes.Amount + var newOps []*rosettatypes.Operation var msgType string // find the fee operation and put it aside for _, op := range ops { @@ -49,7 +278,7 @@ func opsToMsgsAndFees(interfaceRegistry jsonpb.AnyResolver, ops []*types.Operati } // amountsToCoins converts rosetta amounts to sdk coins -func amountsToCoins(amounts []*types.Amount) sdk.Coins { +func amountsToCoins(amounts []*rosettatypes.Amount) sdk.Coins { var feeCoins sdk.Coins for _, amount := range amounts { @@ -65,9 +294,9 @@ func amountsToCoins(amounts []*types.Amount) sdk.Coins { return feeCoins } -func opsToMsgs(interfaceRegistry jsonpb.AnyResolver, ops []*types.Operation) ([]sdk.Msg, error) { +func opsToMsgs(interfaceRegistry jsonpb.AnyResolver, ops []*rosettatypes.Operation) ([]sdk.Msg, error) { var msgs []sdk.Msg - var operationsByType = make(map[string][]*types.Operation) + var operationsByType = make(map[string][]*rosettatypes.Operation) for _, op := range ops { operationsByType[op.Type] = append(operationsByType[op.Type], op) } @@ -93,3 +322,135 @@ func opsToMsgs(interfaceRegistry jsonpb.AnyResolver, ops []*types.Operation) ([] return msgs, nil } + +// ------------------- from tendermint to rosetta ------------------ + +// tmTxsToRosettaTxsIdentifiers converts a tendermint raw transactions into an array of rosetta tx identifiers +func tmTxsToRosettaTxsIdentifiers(txs []tmtypes.Tx) []*rosettatypes.TransactionIdentifier { + converted := make([]*rosettatypes.TransactionIdentifier, len(txs)) + for i, tx := range txs { + converted[i] = tmTxToRosettaTxIdentifier(tx) + } + + return converted +} + +// tmTxToRosettaTxIdentifier converts a tendermint raw transaction into a rosetta tx identifier +func tmTxToRosettaTxIdentifier(tx tmtypes.Tx) *rosettatypes.TransactionIdentifier { + return &rosettatypes.TransactionIdentifier{Hash: fmt.Sprintf("%x", tx.Hash())} +} + +// tmPeersToRosettaPeers converts tendermint peers to rosetta ones +func tmPeersToRosettaPeers(peers []tmcoretypes.Peer) []*rosettatypes.Peer { + converted := make([]*rosettatypes.Peer, len(peers)) + + for i, peer := range peers { + converted[i] = &rosettatypes.Peer{ + PeerID: peer.NodeInfo.Moniker, + Metadata: map[string]interface{}{ + "addr": peer.NodeInfo.ListenAddr, + }, + } + } + + return converted +} + +// tmStatusToRosettaSyncStatus converts a tendermint status to rosetta sync status +func tmStatusToRosettaSyncStatus(status *tmcoretypes.ResultStatus) *rosettatypes.SyncStatus { + // determine sync status + var stage = StatusPeerSynced + if status.SyncInfo.CatchingUp { + stage = StatusPeerSyncing + } + + return &rosettatypes.SyncStatus{ + CurrentIndex: status.SyncInfo.LatestBlockHeight, + TargetIndex: nil, // sync info does not allow us to get target height + Stage: &stage, + } +} + +// tmBlockToRosettaBlockIdentifier converts a tendermint result block to a rosetta block identifier +func tmBlockToRosettaBlockIdentifier(block *tmcoretypes.ResultBlock) *rosettatypes.BlockIdentifier { + return &rosettatypes.BlockIdentifier{ + Index: block.Block.Height, + Hash: block.Block.Hash().String(), + } +} + +// tmBlockToRosettaParentBlockIdentifier returns the parent block identifier from the last block +func tmBlockToRosettaParentBlockIdentifier(block *tmcoretypes.ResultBlock) *rosettatypes.BlockIdentifier { + if block.Block.Height == 1 { + return &rosettatypes.BlockIdentifier{ + Index: 1, + Hash: fmt.Sprintf("%X", block.BlockID.Hash.Bytes()), + } + } + + return &rosettatypes.BlockIdentifier{ + Index: block.Block.Height - 1, + Hash: fmt.Sprintf("%X", block.Block.LastBlockID.Hash.Bytes()), + } +} + +// tmResultBlockToRosettaBlockResponse converts a tendermint result block to block response +func tmResultBlockToRosettaBlockResponse(block *tmcoretypes.ResultBlock) crgtypes.BlockResponse { + return crgtypes.BlockResponse{ + Block: tmBlockToRosettaBlockIdentifier(block), + ParentBlock: tmBlockToRosettaParentBlockIdentifier(block), + MillisecondTimestamp: timeToMilliseconds(block.Block.Time), + TxCount: int64(len(block.Block.Txs)), + } +} + +// --------------------------- end --------------------------------- + +// ----------------------- raw ros utils --------------------------- + +// endBlockTxHash produces a mock endblock hash that rosetta can query +// for endblock operations, it also serves the purpose of representing +// part of the state changes happening at endblock level (balance ones) +func endBlockTxHash(hash []byte) string { + return fmt.Sprintf("%x%x", endBlockHashStart, hash) +} + +// beginBlockTxHash produces a mock beginblock hash that rosetta can query +// for beginblock operations, it also serves the purpose of representing +// part of the state changes happening at beginblock level (balance ones) +func beginBlockTxHash(hash []byte) string { + return fmt.Sprintf("%x%x", beginBlockHashStart, hash) +} + +// normalizeOperationIndexes adds the indexes to operations adhering to specific rules: +// operations related to messages will be always before than the balance ones +func normalizeOperationIndexes(msgOps []*rosettatypes.Operation, balanceOps []*rosettatypes.Operation) (finalOps []*rosettatypes.Operation) { + lenMsgOps := len(msgOps) + lenBalanceOps := len(balanceOps) + finalOps = make([]*rosettatypes.Operation, 0, lenMsgOps+lenBalanceOps) + + var currentIndex int64 + // add indexes to msg ops + for _, op := range msgOps { + op.OperationIdentifier = &rosettatypes.OperationIdentifier{ + Index: currentIndex, + } + + finalOps = append(finalOps, op) + currentIndex++ + } + + // add indexes to balance ops + for _, op := range balanceOps { + op.OperationIdentifier = &rosettatypes.OperationIdentifier{ + Index: currentIndex, + } + + finalOps = append(finalOps, op) + currentIndex++ + } + + return finalOps +} + +// --------------------------- end --------------------------------- diff --git a/server/rosetta/types.go b/server/rosetta/types.go index 626e7470ab91..d96afa5e679c 100644 --- a/server/rosetta/types.go +++ b/server/rosetta/types.go @@ -8,10 +8,11 @@ import ( // statuses const ( - StatusSuccess = "Success" - StatusReverted = "Reverted" - StageSynced = "synced" - StageSyncing = "syncing" + StatusTxSuccess = "Success" + StatusTxReverted = "Reverted" + StatusTxUnconfirmed = "Unconfirmed" + StatusPeerSynced = "synced" + StatusPeerSyncing = "syncing" ) // misc diff --git a/server/rosetta/util.go b/server/rosetta/util.go index 29e4a1587dc2..6e9904f06e46 100644 --- a/server/rosetta/util.go +++ b/server/rosetta/util.go @@ -2,48 +2,14 @@ package rosetta import ( "fmt" + "time" "github.com/coinbase/rosetta-sdk-go/types" - - tmcoretypes "github.com/tendermint/tendermint/rpc/core/types" - tmtypes "github.com/tendermint/tendermint/types" - - sdk "github.com/cosmos/cosmos-sdk/types" ) -// tmResultTxsToSdkTxsWithHash converts tendermint result txs to cosmos sdk.Tx -func tmResultTxsToSdkTxsWithHash(decode sdk.TxDecoder, txs []*tmcoretypes.ResultTx) ([]*sdkTxWithHash, error) { - converted := make([]*sdkTxWithHash, len(txs)) - for i, tx := range txs { - sdkTx, err := decode(tx.Tx) - if err != nil { - return nil, err - } - converted[i] = &sdkTxWithHash{ - HexHash: fmt.Sprintf("%X", tx.Tx.Hash()), - Code: tx.TxResult.Code, - Log: tx.TxResult.Log, - Tx: sdkTx, - } - } - - return converted, nil -} - -func tmTxToSdkTx(decode sdk.TxDecoder, tx tmtypes.Tx) (sdk.Tx, error) { - sdkTx, err := decode(tx) - if err != nil { - return nil, err - } - - return sdkTx, err -} - -type sdkTxWithHash struct { - HexHash string - Code uint32 - Log string - Tx sdk.Tx +// timeToMilliseconds converts time to milliseconds timestamp +func timeToMilliseconds(t time.Time) int64 { + return t.UnixNano() / (int64(time.Millisecond) / int64(time.Nanosecond)) } type PayloadReqMetadata struct { diff --git a/x/distribution/types/msg.go b/x/distribution/types/msg.go index 62c8bfc2abbb..bb72e55a243a 100644 --- a/x/distribution/types/msg.go +++ b/x/distribution/types/msg.go @@ -100,9 +100,9 @@ func (msg *MsgWithdrawDelegatorReward) ToOperations(withStatus, hasError bool) [ var status string if withStatus { - status = rosetta.StatusSuccess + status = rosetta.StatusTxSuccess if hasError { - status = rosetta.StatusReverted + status = rosetta.StatusTxReverted } } diff --git a/x/staking/types/msg.go b/x/staking/types/msg.go index eda27b768a32..38166581e546 100644 --- a/x/staking/types/msg.go +++ b/x/staking/types/msg.go @@ -273,9 +273,9 @@ func (msg *MsgDelegate) ToOperations(withStatus bool, hasError bool) []*rosettat delOp := func(account *rosettatypes.AccountIdentifier, amount string, index int) *rosettatypes.Operation { var status string if withStatus { - status = rosetta.StatusSuccess + status = rosetta.StatusTxSuccess if hasError { - status = rosetta.StatusReverted + status = rosetta.StatusTxReverted } } return &rosettatypes.Operation{ @@ -413,9 +413,9 @@ func (msg *MsgBeginRedelegate) ToOperations(withStatus bool, hasError bool) []*r delOp := func(account *rosettatypes.AccountIdentifier, amount string, index int) *rosettatypes.Operation { var status string if withStatus { - status = rosetta.StatusSuccess + status = rosetta.StatusTxSuccess if hasError { - status = rosetta.StatusReverted + status = rosetta.StatusTxReverted } } return &rosettatypes.Operation{ @@ -557,9 +557,9 @@ func (msg *MsgUndelegate) ToOperations(withStatus bool, hasError bool) []*rosett delOp := func(account *rosettatypes.AccountIdentifier, amount string, index int) *rosettatypes.Operation { var status string if withStatus { - status = rosetta.StatusSuccess + status = rosetta.StatusTxSuccess if hasError { - status = rosetta.StatusReverted + status = rosetta.StatusTxReverted } } return &rosettatypes.Operation{ From ab91ce3ac8c83bc17c95a37667b27eb154ad9626 Mon Sep 17 00:00:00 2001 From: Frojdi Dymylja Date: Wed, 3 Mar 2021 08:57:12 +0100 Subject: [PATCH 03/33] add: converter --- server/rosetta/convert/converter.go | 435 +++++++++++++++++++++++ server/rosetta/convert/converter_test.go | 118 ++++++ 2 files changed, 553 insertions(+) create mode 100644 server/rosetta/convert/converter.go create mode 100644 server/rosetta/convert/converter_test.go diff --git a/server/rosetta/convert/converter.go b/server/rosetta/convert/converter.go new file mode 100644 index 000000000000..e596f67a5522 --- /dev/null +++ b/server/rosetta/convert/converter.go @@ -0,0 +1,435 @@ +package convert + +import ( + "encoding/json" + "fmt" + "reflect" + + rosettatypes "github.com/coinbase/rosetta-sdk-go/types" + "github.com/gogo/protobuf/proto" + crgerrs "github.com/tendermint/cosmos-rosetta-gateway/errors" + abci "github.com/tendermint/tendermint/abci/types" + tmtypes "github.com/tendermint/tendermint/types" + + sdkclient "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/codec" + codectypes "github.com/cosmos/cosmos-sdk/codec/types" + "github.com/cosmos/cosmos-sdk/server/rosetta/types" + sdk "github.com/cosmos/cosmos-sdk/types" + authsigning "github.com/cosmos/cosmos-sdk/x/auth/signing" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" +) + +// converter is a utility that can be used to convert +// back and forth from rosetta to sdk and tendermint types +type Converter interface { + FromRosetta() FromRosettaConverter + ToRosetta() ToRosettaConverter +} + +// ToRosettaConverter is an interface that exposes +// all the functions used to convert sdk and +// tendermint types to rosetta known types +type ToRosettaConverter interface { + // CoinsToAmounts converts sdk.Coins to amounts + CoinsToAmounts(ownedCoins []sdk.Coin, availableCoins sdk.Coins) []*rosettatypes.Amount + // MsgToOps converts an sdk.Msg to a rosetta operation + MsgToOps(status string, msg sdk.Msg) ([]*rosettatypes.Operation, error) + // MsgToMeta converts an sdk.Msg to rosetta metadata + MsgToMeta(msg sdk.Msg) (meta map[string]interface{}, err error) + // Tx converts a tendermint transaction and tx result if provided to a rosetta tx + Tx(rawTx tmtypes.Tx, txResult *abci.ResponseDeliverTx) (*rosettatypes.Transaction, error) + // EventsToBalanceOps converts events to balance operations + EventsToBalanceOps(status string, events []abci.Event) []*rosettatypes.Operation +} + +// FromRosettaConverter is an interface that exposes +// all the functions used to convert rosetta types +// to tendermint and sdk types +type FromRosettaConverter interface { + OpsToUnsignedTx(ops []*rosettatypes.Operation) (tx authsigning.Tx, err error) + MetaToMsg(meta map[string]interface{}, msg sdk.Msg) (err error) +} + +type converter struct { + newTxBuilder func() sdkclient.TxBuilder + txDecode sdk.TxDecoder + ir codectypes.InterfaceRegistry + cdc *codec.ProtoCodec +} + +func NewConverter(cdc *codec.ProtoCodec, ir codectypes.InterfaceRegistry, cfg sdkclient.TxConfig) Converter { + return converter{ + newTxBuilder: func() sdkclient.TxBuilder { + return cfg.NewTxBuilder() + }, + ir: ir, + cdc: cdc, + txDecode: cfg.TxDecoder(), + } +} + +func (c converter) FromRosetta() FromRosettaConverter { + return c +} + +func (c converter) ToRosetta() ToRosettaConverter { + return c +} + +// OpsToUnsignedTx returns all the sdk.Msgs given the operations +func (c converter) OpsToUnsignedTx(ops []*rosettatypes.Operation) (tx authsigning.Tx, err error) { + builder := c.newTxBuilder() + + var msgs []sdk.Msg + + for i := 0; i < len(ops); i++ { + op := ops[i] + + protoMessage, err := c.ir.Resolve("/" + op.Type) + if err != nil { + return nil, crgerrs.WrapError(crgerrs.ErrBadArgument, "operation not found: "+op.Type) + } + + msg, ok := protoMessage.(sdk.Msg) + if !ok { + return nil, crgerrs.WrapError(crgerrs.ErrBadArgument, "operation is not a valid supported sdk.Msg: "+op.Type) + } + + err = c.MetaToMsg(op.Metadata, msg) + if err != nil { + return nil, crgerrs.WrapError(crgerrs.ErrCodec, err.Error()) + } + + // verify message correctness + if err = msg.ValidateBasic(); err != nil { + return nil, crgerrs.WrapError( + crgerrs.ErrBadArgument, + fmt.Sprintf("validation of operation at index %d failed: %s", op.OperationIdentifier.Index, err), + ) + } + signers := msg.GetSigners() + // check if there are enough signers + if len(signers) == 0 { + return nil, crgerrs.WrapError(crgerrs.ErrBadArgument, fmt.Sprintf("operation at index %d got no signers", op.OperationIdentifier.Index)) + } + // append the msg + msgs = append(msgs, msg) + // if there's only one signer then simply continue + if len(signers) == 1 { + continue + } + // after we have got the msg, we need to verify if the message has multiple signers + // if it has got multiple signers, then we need to fetch all the related operations + // which involve the other signers of the msg, we expect to find them in order + // so if the msg is named "v1.test.Send" and it expects 3 signers, the next 3 operations + // must be with the same name "v1.test.Send" and contain the other signers + // then we can just skip their processing + for j := 0; j < len(signers)-1; j++ { + skipOp := ops[i+j] // get the next index + // verify that the operation is equal to the new one + if skipOp.Type != op.Type { + return nil, crgerrs.WrapError( + crgerrs.ErrBadArgument, + fmt.Sprintf("operation at index %d should have had type %s got: %s", i+j, op.Type, skipOp.Type), + ) + } + + if !reflect.DeepEqual(op.Metadata, skipOp.Metadata) { + return nil, crgerrs.WrapError( + crgerrs.ErrBadArgument, + fmt.Sprintf("operation at index %d should have had metadata equal to %#v, got: %#v", i+j, op.Metadata, skipOp.Metadata)) + } + + i++ // increase so we skip it + } + } + + if err := builder.SetMsgs(msgs...); err != nil { + return nil, crgerrs.WrapError(crgerrs.ErrBadArgument, err.Error()) + } + + return builder.GetTx(), nil + +} + +// MetaToMsg unmarshals the rosetta metadata to the given sdk.Msg +func (c converter) MetaToMsg(meta map[string]interface{}, msg sdk.Msg) error { + metaBytes, err := json.Marshal(meta) + if err != nil { + return err + } + return c.cdc.UnmarshalJSON(metaBytes, msg) +} + +func (c converter) MsgToMeta(msg sdk.Msg) (meta map[string]interface{}, err error) { + b, err := c.cdc.MarshalJSON(msg) + if err != nil { + return nil, crgerrs.WrapError(crgerrs.ErrCodec, err.Error()) + } + + err = json.Unmarshal(b, &meta) + if err != nil { + return nil, crgerrs.WrapError(crgerrs.ErrCodec, err.Error()) + } + + return +} + +// MsgToOps will create an operation for each msg signer +// with the message proto name as type, and the raw fields +// as metadata +func (c converter) MsgToOps(status string, msg sdk.Msg) ([]*rosettatypes.Operation, error) { + opName := proto.MessageName(msg) + // in case proto does not recognize the message name + // then we should try to cast it to service msg, to + // check if it was wrapped or not, in case the cast + // from sdk.ServiceMsg to sdk.Msg fails, then a + // codec error is returned + if opName == "" { + unwrappedMsg, ok := msg.(sdk.ServiceMsg) + if !ok { + return nil, crgerrs.WrapError(crgerrs.ErrCodec, fmt.Sprintf("unrecognized message type: %T", msg)) + } + + msg, ok = unwrappedMsg.Request.(sdk.Msg) + if !ok { + return nil, crgerrs.WrapError( + crgerrs.ErrCodec, + fmt.Sprintf("unable to cast %T to sdk.Msg, method: %s", unwrappedMsg, unwrappedMsg.MethodName), + ) + } + + opName = proto.MessageName(msg) + if opName == "" { + return nil, crgerrs.WrapError(crgerrs.ErrCodec, fmt.Sprintf("unrecognized message type: %T", msg)) + } + } + + meta, err := c.MsgToMeta(msg) + if err != nil { + return nil, err + } + + ops := make([]*rosettatypes.Operation, len(msg.GetSigners())) + for i, signer := range msg.GetSigners() { + op := &rosettatypes.Operation{ + Type: opName, + Status: status, + Account: &rosettatypes.AccountIdentifier{Address: signer.String()}, + Metadata: meta, + } + + ops[i] = op + } + + return ops, nil +} + +// Tx converts a tendermint raw transaction and its result (if provided) to a rosetta transaction +func (c converter) Tx(rawTx tmtypes.Tx, txResult *abci.ResponseDeliverTx) (*rosettatypes.Transaction, error) { + // decode tx + tx, err := c.txDecode(rawTx) + if err != nil { + return nil, crgerrs.WrapError(crgerrs.ErrCodec, err.Error()) + } + // get initial status, as per sdk design, if one msg fails + // the whole TX will be considered failing, so we can't have + // 1 msg being success and 1 msg being reverted + status := types.StatusTxSuccess + switch txResult { + // if nil, we're probably checking an unconfirmed tx + case nil: + status = types.StatusTxUnconfirmed + // set the status + default: + if txResult.Code != abci.CodeTypeOK { + status = types.StatusTxReverted + } + } + // get operations from msgs + msgs := tx.GetMsgs() + var rawTxOps []*rosettatypes.Operation + for _, msg := range msgs { + ops, err := c.MsgToOps(status, msg) + if err != nil { + return nil, err + } + rawTxOps = append(rawTxOps, ops...) + } + + // now get balance events from response deliver tx + var balanceOps []*rosettatypes.Operation + // tx result might be nil, in case we're querying an unconfirmed tx from the mempool + if txResult != nil { + balanceOps = c.EventsToBalanceOps(status, txResult.Events) + } + + // now normalize indexes + totalOps := AddOperationIndexes(rawTxOps, balanceOps) + + return &rosettatypes.Transaction{ + TransactionIdentifier: &rosettatypes.TransactionIdentifier{Hash: fmt.Sprintf("%x", rawTx.Hash())}, + Operations: totalOps, + }, nil +} + +func (c converter) EventsToBalanceOps(status string, events []abci.Event) []*rosettatypes.Operation { + var ops []*rosettatypes.Operation + + for _, e := range events { + balanceOps, ok := sdkEventToBalanceOperations(status, e) + if !ok { + continue + } + ops = append(ops, balanceOps...) + } + + return ops +} + +// sdkEventToBalanceOperations converts an event to a rosetta balance operation +// it will panic if the event is malformed because it might mean the sdk spec +// has changed and rosetta needs to reflect those changes too. +// The balance operations are multiple, one for each denom. +func sdkEventToBalanceOperations(status string, event abci.Event) (operations []*rosettatypes.Operation, isBalanceEvent bool) { + + var ( + accountIdentifier string + coinChange sdk.Coins + isSub bool + ) + + switch event.Type { + default: + return nil, false + case banktypes.EventTypeCoinSpent: + spender, err := sdk.AccAddressFromBech32((string)(event.Attributes[0].Value)) + if err != nil { + panic(err) + } + coins, err := sdk.ParseCoinsNormalized((string)(event.Attributes[1].Value)) + if err != nil { + panic(err) + } + + isSub = true + coinChange = coins + accountIdentifier = spender.String() + + case banktypes.EventTypeCoinReceived: + receiver, err := sdk.AccAddressFromBech32((string)(event.Attributes[0].Value)) + if err != nil { + panic(err) + } + coins, err := sdk.ParseCoinsNormalized((string)(event.Attributes[1].Value)) + if err != nil { + panic(err) + } + + isSub = false + coinChange = coins + accountIdentifier = receiver.String() + + // rosetta does not have the concept of burning coins, so we need to mock + // the burn as a send to an address that cannot be resolved to anything + case banktypes.EventTypeCoinBurn: + coins, err := sdk.ParseCoinsNormalized((string)(event.Attributes[1].Value)) + if err != nil { + panic(err) + } + + coinChange = coins + accountIdentifier = types.BurnerAddressIdentifier + } + + operations = make([]*rosettatypes.Operation, len(coinChange)) + + for i, coin := range coinChange { + + value := coin.Amount.String() + // in case the event is a subtract balance one the rewrite value with + // the negative coin identifier + if isSub { + value = "-" + value + } + + op := &rosettatypes.Operation{ + Type: event.Type, + Status: status, + Account: &rosettatypes.AccountIdentifier{Address: accountIdentifier}, + Amount: &rosettatypes.Amount{ + Value: value, + Currency: &rosettatypes.Currency{ + Symbol: coin.Denom, + Decimals: 0, + }, + }, + } + + operations[i] = op + } + return operations, true +} + +// sdkCoinsToRosettaAmounts converts []sdk.Coin to rosetta amounts +func (c converter) CoinsToAmounts(ownedCoins []sdk.Coin, availableCoins sdk.Coins) []*rosettatypes.Amount { + amounts := make([]*rosettatypes.Amount, len(availableCoins)) + ownedCoinsMap := make(map[string]sdk.Int, len(availableCoins)) + + for _, ownedCoin := range ownedCoins { + ownedCoinsMap[ownedCoin.Denom] = ownedCoin.Amount + } + + for i, coin := range availableCoins { + value, owned := ownedCoinsMap[coin.Denom] + if !owned { + amounts[i] = &rosettatypes.Amount{ + Value: sdk.NewInt(0).String(), + Currency: &rosettatypes.Currency{ + Symbol: coin.Denom, + }, + } + continue + } + amounts[i] = &rosettatypes.Amount{ + Value: value.String(), + Currency: &rosettatypes.Currency{ + Symbol: coin.Denom, + }, + } + } + + return amounts +} + +// AddOperationIndexes adds the indexes to operations adhering to specific rules: +// operations related to messages will be always before than the balance ones +func AddOperationIndexes(msgOps []*rosettatypes.Operation, balanceOps []*rosettatypes.Operation) (finalOps []*rosettatypes.Operation) { + lenMsgOps := len(msgOps) + lenBalanceOps := len(balanceOps) + finalOps = make([]*rosettatypes.Operation, 0, lenMsgOps+lenBalanceOps) + + var currentIndex int64 + // add indexes to msg ops + for _, op := range msgOps { + op.OperationIdentifier = &rosettatypes.OperationIdentifier{ + Index: currentIndex, + } + + finalOps = append(finalOps, op) + currentIndex++ + } + + // add indexes to balance ops + for _, op := range balanceOps { + op.OperationIdentifier = &rosettatypes.OperationIdentifier{ + Index: currentIndex, + } + + finalOps = append(finalOps, op) + currentIndex++ + } + + return finalOps +} diff --git a/server/rosetta/convert/converter_test.go b/server/rosetta/convert/converter_test.go new file mode 100644 index 000000000000..5b635d817e41 --- /dev/null +++ b/server/rosetta/convert/converter_test.go @@ -0,0 +1,118 @@ +package convert + +import ( + "testing" + + rosettatypes "github.com/coinbase/rosetta-sdk-go/types" + "github.com/stretchr/testify/suite" + crgerrs "github.com/tendermint/cosmos-rosetta-gateway/errors" + + "github.com/cosmos/cosmos-sdk/codec" + codectypes "github.com/cosmos/cosmos-sdk/codec/types" + cryptocodec "github.com/cosmos/cosmos-sdk/crypto/codec" + sdk "github.com/cosmos/cosmos-sdk/types" + authtx "github.com/cosmos/cosmos-sdk/x/auth/tx" + auth "github.com/cosmos/cosmos-sdk/x/auth/types" + bank "github.com/cosmos/cosmos-sdk/x/bank/types" +) + +type ConverterTestSuite struct { + suite.Suite + + c Converter +} + +func (s *ConverterTestSuite) SetupTest() { + ir := codectypes.NewInterfaceRegistry() + cdc := codec.NewProtoCodec(ir) + + auth.RegisterInterfaces(ir) + bank.RegisterInterfaces(ir) + cryptocodec.RegisterInterfaces(ir) + txConfig := authtx.NewTxConfig(cdc, authtx.DefaultSignModes) + s.c = NewConverter(cdc, ir, txConfig) +} + +func (s *ConverterTestSuite) TestFromRosettaOpsToTxSuccess() { + addr1 := sdk.AccAddress("address1").String() + addr2 := sdk.AccAddress("address2").String() + + msg1 := &bank.MsgSend{ + FromAddress: addr1, + ToAddress: addr2, + Amount: sdk.NewCoins(sdk.NewInt64Coin("test", 10)), + } + + msg2 := &bank.MsgSend{ + FromAddress: addr2, + ToAddress: addr1, + Amount: sdk.NewCoins(sdk.NewInt64Coin("utxo", 10)), + } + + ops, err := s.c.ToRosetta().MsgToOps("", msg1) + s.Require().NoError(err) + + ops2, err := s.c.ToRosetta().MsgToOps("", msg2) + s.Require().NoError(err) + + ops = append(ops, ops2...) + + tx, err := s.c.FromRosetta().OpsToUnsignedTx(ops) + s.Require().NoError(err) + + msgs := tx.GetMsgs() + + s.Require().Equal(2, len(msgs)) + + s.Require().Equal(msgs[0], msg1) + s.Require().Equal(msgs[1], msg2) + +} + +func (s *ConverterTestSuite) TestFromRosettaOpsToTxErrors() { + s.Run("unrecognized op", func() { + op := &rosettatypes.Operation{ + Type: "non-existent", + } + + _, err := s.c.FromRosetta().OpsToUnsignedTx([]*rosettatypes.Operation{op}) + + s.Require().ErrorIs(err, crgerrs.ErrBadArgument) + }) + + s.Run("codec type but not sdk.Msg", func() { + op := &rosettatypes.Operation{ + Type: "cosmos.crypto.ed25519.PubKey", + } + + _, err := s.c.FromRosetta().OpsToUnsignedTx([]*rosettatypes.Operation{op}) + + s.Require().ErrorIs(err, crgerrs.ErrBadArgument) + + }) + +} + +func (s *ConverterTestSuite) TestMsgToMetaMetaToMsg() { + msg := &bank.MsgSend{ + FromAddress: "addr1", + ToAddress: "addr2", + Amount: sdk.NewCoins(sdk.NewInt64Coin("test", 10)), + } + + msg.Route() + + meta, err := s.c.ToRosetta().MsgToMeta(msg) + s.Require().NoError(err) + + copyMsg := new(bank.MsgSend) + + err = s.c.FromRosetta().MetaToMsg(meta, copyMsg) + s.Require().NoError(err) + + s.Require().Equal(msg, copyMsg) +} + +func TestConverterTestSuite(t *testing.T) { + suite.Run(t, new(ConverterTestSuite)) +} From b3bf68bbd3de4f4c228cf96c3d2df3276ce79fc3 Mon Sep 17 00:00:00 2001 From: Frojdi Dymylja Date: Wed, 3 Mar 2021 09:00:06 +0100 Subject: [PATCH 04/33] fix: casting error message --- server/rosetta/convert/converter.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/rosetta/convert/converter.go b/server/rosetta/convert/converter.go index e596f67a5522..ee0b6f3d0276 100644 --- a/server/rosetta/convert/converter.go +++ b/server/rosetta/convert/converter.go @@ -196,7 +196,7 @@ func (c converter) MsgToOps(status string, msg sdk.Msg) ([]*rosettatypes.Operati if !ok { return nil, crgerrs.WrapError( crgerrs.ErrCodec, - fmt.Sprintf("unable to cast %T to sdk.Msg, method: %s", unwrappedMsg, unwrappedMsg.MethodName), + fmt.Sprintf("unable to cast %T to sdk.Msg, method: %s", unwrappedMsg.Request, unwrappedMsg.MethodName), ) } From 593f26ddb712dcb1c601b2274c386f4e19a8432a Mon Sep 17 00:00:00 2001 From: Frojdi Dymylja Date: Thu, 4 Mar 2021 12:54:52 +0100 Subject: [PATCH 05/33] change: rework construction API to support every possible transaction type --- contrib/rosetta/configuration/rosetta.json | 2 +- contrib/rosetta/configuration/transfer.ros | 40 +- server/rosetta/client_offline.go | 140 +++--- server/rosetta/client_online.go | 268 ++++++---- server/rosetta/client_v2.go | 115 ----- server/rosetta/client_v2_test.go | 34 ++ server/rosetta/conv_from_rosetta.go | 108 ----- server/rosetta/conv_to_rosetta.go | 456 ------------------ server/rosetta/{convert => }/converter.go | 134 ++++- .../rosetta/{convert => }/converter_test.go | 13 +- server/rosetta/types.go | 98 +++- server/rosetta/util.go | 80 +-- x/bank/types/msgs.go | 84 ---- x/distribution/types/msg.go | 50 -- x/staking/types/msg.go | 275 ----------- 15 files changed, 528 insertions(+), 1369 deletions(-) delete mode 100644 server/rosetta/client_v2.go delete mode 100644 server/rosetta/conv_from_rosetta.go delete mode 100644 server/rosetta/conv_to_rosetta.go rename server/rosetta/{convert => }/converter.go (73%) rename server/rosetta/{convert => }/converter_test.go (86%) diff --git a/contrib/rosetta/configuration/rosetta.json b/contrib/rosetta/configuration/rosetta.json index 39a0bb3811dd..b4adc6a756f1 100644 --- a/contrib/rosetta/configuration/rosetta.json +++ b/contrib/rosetta/configuration/rosetta.json @@ -25,7 +25,7 @@ "constructor_dsl_file": "transfer.ros", "end_conditions": { "create_account": 1, - "transfer": 3 + "transfer": 1 } }, "data": { diff --git a/contrib/rosetta/configuration/transfer.ros b/contrib/rosetta/configuration/transfer.ros index a1cb3f8caf89..74ebd2ddf50c 100644 --- a/contrib/rosetta/configuration/transfer.ros +++ b/contrib/rosetta/configuration/transfer.ros @@ -26,7 +26,7 @@ request_funds(1){ loaded_account = find_balance({ "account_identifier": {{random_account.account_identifier}}, "minimum_balance":{ - "value": "100", + "value": "50", "currency": {{currency}} } }); @@ -57,6 +57,8 @@ transfer(3){ "currency": {{currency}} } }); + acc_identifier = {{sender.account_identifier}}; + sender_address = {{acc_identifier.address}}; // Set the recipient_amount as some value <= sender.balance-max_fee max_fee = "0"; fee_amount = "1"; @@ -76,34 +78,28 @@ transfer(3){ "create_probability": 50 }); transfer.confirmation_depth = "1"; + recipient_account_identifier = {{recipient.account_identifier}}; + recipient_address = {{recipient_account_identifier.address}}; transfer.operations = [ { "operation_identifier":{"index":0}, - "type":"fee", - "account":{{sender.account_identifier}}, - "amount":{ - "value":{{fee_value}}, - "currency":{{currency}} - } - }, - { - "operation_identifier":{"index":1}, "type":"cosmos.bank.v1beta1.MsgSend", "account":{{sender.account_identifier}}, - "amount":{ - "value":{{sender_amount}}, - "currency":{{currency}} - } - }, - { - "operation_identifier":{"index":2}, - "type":"cosmos.bank.v1beta1.MsgSend", - "account":{{recipient.account_identifier}}, - "amount":{ - "value":{{recipient_amount}}, - "currency":{{currency}} + "metadata": { + "amount": [ + { + "amount": {{recipient_amount}}, + "denom": {{currency.symbol}} + } + ], + "from_address": {{sender_address}}, + "to_address": {{recipient_address}} } } ]; + transfer.preprocess_metadata = { + "gas_price": "1stake", + "gas_limit": 250000 + }; } } diff --git a/server/rosetta/client_offline.go b/server/rosetta/client_offline.go index dd1dc175f241..0495a5cffed6 100644 --- a/server/rosetta/client_offline.go +++ b/server/rosetta/client_offline.go @@ -3,20 +3,22 @@ package rosetta import ( "context" "encoding/hex" + "log" "strings" + "github.com/tendermint/tendermint/crypto" + + authsigning "github.com/cosmos/cosmos-sdk/x/auth/signing" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" "github.com/btcsuite/btcd/btcec" "github.com/coinbase/rosetta-sdk-go/types" crgerrs "github.com/tendermint/cosmos-rosetta-gateway/errors" - "github.com/tendermint/tendermint/crypto" - "github.com/cosmos/cosmos-sdk/client/tx" "github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/tx/signing" - authsigning "github.com/cosmos/cosmos-sdk/x/auth/signing" ) func (c *Client) OperationStatuses() []*types.OperationStatus { @@ -29,10 +31,6 @@ func (c *Client) OperationStatuses() []*types.OperationStatus { Status: StatusTxReverted, Successful: false, }, - { - Status: StatusTxUnconfirmed, - Successful: false, - }, } } @@ -48,14 +46,14 @@ func (c *Client) SupportedOperations() []string { continue } - if _, ok := resolvedMsg.(Msg); ok { + if _, ok := resolvedMsg.(sdk.Msg); ok { supportedOperations = append(supportedOperations, strings.TrimLeft(ii, "/")) } } supportedOperations = append( supportedOperations, - OperationFee, banktypes.EventTypeCoinSpent, banktypes.EventTypeCoinReceived, + banktypes.EventTypeCoinSpent, banktypes.EventTypeCoinReceived, ) return supportedOperations @@ -122,57 +120,56 @@ func (c *Client) ConstructionPayload(_ context.Context, request *types.Construct return nil, crgerrs.WrapError(crgerrs.ErrInvalidOperation, "expected at least one operation") } - // convert rosetta operations to sdk msgs and fees (if present) - msgs, fee, err := opsToMsgsAndFees(c.ir, request.Operations) + tx, err := c.converter.FromRosetta().OpsToUnsignedTx(request.Operations) if err != nil { return nil, crgerrs.WrapError(crgerrs.ErrInvalidOperation, err.Error()) } - metadata, err := getMetadataFromPayloadReq(request) - if err != nil { + metadata := new(ConstructionMetadata) + if err = metadata.FromMetadata(request.Metadata); err != nil { return nil, crgerrs.WrapError(crgerrs.ErrBadArgument, err.Error()) } - - txFactory := tx.Factory{}.WithAccountNumber(metadata.AccountNumber).WithChainID(metadata.ChainID). - WithGas(metadata.Gas).WithSequence(metadata.Sequence).WithMemo(metadata.Memo).WithFees(fee.String()) - - TxConfig := c.getTxConfig() - txFactory = txFactory.WithTxConfig(TxConfig) - - txBldr, err := tx.BuildUnsignedTx(txFactory, msgs...) + feeAmt, err := sdk.ParseCoinsNormalized(metadata.GasPrice) if err != nil { - return nil, err + return nil, crgerrs.WrapError(crgerrs.ErrBadArgument, err.Error()) } - // Sign_mode_legacy_amino is being used as default here, as sign_mode_direct - // needs the signer infos to be set before hand but rosetta doesn't have a way - // to do this yet. To be revisited in future versions of sdk and rosetta - if txFactory.SignMode() == signing.SignMode_SIGN_MODE_UNSPECIFIED { - txFactory = txFactory.WithSignMode(signing.SignMode_SIGN_MODE_LEGACY_AMINO_JSON) - } + // + builder := c.getTxConfig().NewTxBuilder() + _ = builder.SetMsgs(tx.GetMsgs()...) + builder.SetFeeAmount(feeAmt) + builder.SetGasLimit(metadata.GasLimit) + builder.SetMemo(metadata.Memo) - signerData := authsigning.SignerData{ - ChainID: txFactory.ChainID(), - AccountNumber: txFactory.AccountNumber(), - Sequence: txFactory.Sequence(), - } - - signBytes, err := TxConfig.SignModeHandler().GetSignBytes(txFactory.SignMode(), signerData, txBldr.GetTx()) + tx = builder.GetTx() + txBytes, err := c.clientCtx.TxConfig.TxEncoder()(builder.GetTx()) if err != nil { return nil, err } - txBytes, err := TxConfig.TxEncoder()(txBldr.GetTx()) - if err != nil { - return nil, err - } - - accIdentifiers := getAccountIdentifiersByMsgs(msgs) + accIdentifiers := tx.GetSigners() + rawJSONTx, err := c.clientCtx.TxConfig.TxJSONEncoder()(builder.GetTx()) + log.Printf("raw tx: %s", rawJSONTx) payloads := make([]*types.SigningPayload, len(accIdentifiers)) for i, accID := range accIdentifiers { + // we expect pubkeys to be ordered... TODO(fdymylja): maybe make ordering not matter? + signerData := authsigning.SignerData{ + ChainID: metadata.ChainID, + AccountNumber: metadata.SignersData[i].AccountNumber, + Sequence: metadata.SignersData[i].Sequence, + } + + // Sign_mode_legacy_amino is being used as default here, as sign_mode_direct + // needs the signer infos to be set before hand but rosetta doesn't have a way + // to do this yet. To be revisited in future versions of sdk and rosetta + signBytes, err := c.clientCtx.TxConfig.SignModeHandler().GetSignBytes(signing.SignMode_SIGN_MODE_LEGACY_AMINO_JSON, signerData, tx) + if err != nil { + return nil, crgerrs.WrapError(crgerrs.ErrBadArgument, "signing error: "+err.Error()) + } + payloads[i] = &types.SigningPayload{ - AccountIdentifier: accID, + AccountIdentifier: &types.AccountIdentifier{Address: accID.String()}, Bytes: crypto.Sha256(signBytes), SignatureType: types.Ecdsa, } @@ -184,49 +181,48 @@ func (c *Client) ConstructionPayload(_ context.Context, request *types.Construct }, nil } -func getAccountIdentifiersByMsgs(msgs []sdk.Msg) []*types.AccountIdentifier { - var accIdentifiers []*types.AccountIdentifier - for _, msg := range msgs { - for _, signer := range msg.GetSigners() { - accIdentifiers = append(accIdentifiers, &types.AccountIdentifier{Address: signer.String()}) - } +func (c *Client) PreprocessOperationsToOptions(_ context.Context, req *types.ConstructionPreprocessRequest) (optionsMeta map[string]interface{}, err error) { + if len(req.Operations) == 0 { + return nil, crgerrs.WrapError(crgerrs.ErrBadArgument, "no operations") } - return accIdentifiers -} - -func (c *Client) PreprocessOperationsToOptions(_ context.Context, req *types.ConstructionPreprocessRequest) (options map[string]interface{}, err error) { - operations := req.Operations - if len(operations) < 1 { - return nil, crgerrs.WrapError(crgerrs.ErrBadArgument, "invalid number of operations") + // now we need to parse the operations to cosmos sdk messages + tx, err := c.converter.FromRosetta().OpsToUnsignedTx(req.Operations) + if err != nil { + return nil, err } - msgs, err := opsToMsgs(c.ir, operations) + // get the signers + signers := tx.GetSigners() + signersStr := make([]string, len(signers)) + + for i, sig := range signers { + signersStr[i] = sig.String() + } + // get the metadata request information + meta := new(ConstructionPreprocessMetadata) + err = meta.FromMetadata(req.Metadata) if err != nil { - return nil, crgerrs.WrapError(crgerrs.ErrInvalidOperation, err.Error()) + return nil, err } - if len(msgs) < 1 || len(msgs[0].GetSigners()) < 1 { - return nil, crgerrs.WrapError(crgerrs.ErrInvalidOperation, "operation produced no msg or signers") + if meta.GasPrice == "" { + return nil, crgerrs.WrapError(crgerrs.ErrBadArgument, "no gas prices") } - memo, ok := req.Metadata["memo"] - if !ok { - memo = "" + if meta.GasLimit == 0 { + return nil, crgerrs.WrapError(crgerrs.ErrBadArgument, "no gas limit") } - defaultGas := float64(200000) - - gas := req.SuggestedFeeMultiplier - if gas == nil { - gas = &defaultGas + // prepare the options to return + options := &PreprocessOperationsOptionsResponse{ + ExpectedSigners: signersStr, + Memo: meta.Memo, + GasLimit: meta.GasLimit, + GasPrice: meta.GasPrice, } - return map[string]interface{}{ - OptionAddress: msgs[0].GetSigners()[0], - OptionMemo: memo, - OptionGas: gas, - }, nil + return options.ToMetadata() } func (c *Client) AccountIdentifierFromPublicKey(pubKey *types.PublicKey) (*types.AccountIdentifier, error) { diff --git a/server/rosetta/client_online.go b/server/rosetta/client_online.go index 9adf61374e0e..68b862994d3a 100644 --- a/server/rosetta/client_online.go +++ b/server/rosetta/client_online.go @@ -3,9 +3,10 @@ package rosetta import ( "bytes" "context" - "crypto/sha256" "encoding/hex" + "encoding/json" "fmt" + "log" "strconv" "time" @@ -13,7 +14,7 @@ import ( abcitypes "github.com/tendermint/tendermint/abci/types" - "github.com/coinbase/rosetta-sdk-go/types" + rosettatypes "github.com/coinbase/rosetta-sdk-go/types" "google.golang.org/grpc/metadata" "github.com/tendermint/tendermint/rpc/client/http" @@ -51,6 +52,8 @@ type Client struct { txDecoder sdk.TxDecoder version string + + converter Converter } // NewClient instantiates a new online servicer @@ -67,6 +70,7 @@ func NewClient(cfg *Config) (*Client, error) { ir: cfg.InterfaceRegistry, version: fmt.Sprintf("%s/%s", info.AppName, v), txDecoder: authtx.NewTxConfig(cfg.Codec, authtx.DefaultSignModes).TxDecoder(), + converter: NewConverter(cfg.Codec, cfg.InterfaceRegistry, authtx.NewTxConfig(cfg.Codec, authtx.DefaultSignModes)), }, nil } @@ -92,7 +96,7 @@ func (c *Client) accountInfo(ctx context.Context, addr string, height *int64) (a return account, nil } -func (c *Client) Balances(ctx context.Context, addr string, height *int64) ([]*types.Amount, error) { +func (c *Client) Balances(ctx context.Context, addr string, height *int64) ([]*rosettatypes.Amount, error) { if height != nil { strHeight := strconv.FormatInt(*height, 10) ctx = metadata.AppendToOutgoingContext(ctx, grpctypes.GRPCBlockHeightHeader, strHeight) @@ -110,7 +114,7 @@ func (c *Client) Balances(ctx context.Context, addr string, height *int64) ([]*t return nil, err } - return sdkCoinsToRosettaAmounts(balance.Balances, availableCoins), nil + return c.converter.ToRosetta().CoinsToAmounts(balance.Balances, availableCoins), nil } func (c *Client) BlockByHash(ctx context.Context, hash string) (crgtypes.BlockResponse, error) { @@ -121,19 +125,19 @@ func (c *Client) BlockByHash(ctx context.Context, hash string) (crgtypes.BlockRe block, err := c.clientCtx.Client.BlockByHash(ctx, bHash) if err != nil { - return crgtypes.BlockResponse{}, err + return crgtypes.BlockResponse{}, crgerrs.WrapError(crgerrs.ErrBadGateway, err.Error()) } - return tmResultBlockToRosettaBlockResponse(block), nil + return c.converter.ToRosetta().BlockResponse(block), nil } func (c *Client) BlockByHeight(ctx context.Context, height *int64) (crgtypes.BlockResponse, error) { block, err := c.clientCtx.Client.Block(ctx, height) if err != nil { - return crgtypes.BlockResponse{}, err + return crgtypes.BlockResponse{}, crgerrs.WrapError(crgerrs.ErrBadGateway, err.Error()) } - return tmResultBlockToRosettaBlockResponse(block), nil + return c.converter.ToRosetta().BlockResponse(block), nil } func (c *Client) BlockTransactionsByHash(ctx context.Context, hash string) (crgtypes.BlockTransactionsResponse, error) { @@ -151,7 +155,8 @@ func (c *Client) BlockTransactionsByHeight(ctx context.Context, height *int64) ( if err != nil { return crgtypes.BlockTransactionsResponse{}, err } - + bb, _ := json.Marshal(blockTxResp) + log.Printf("block %d: %s", height, bb) return blockTxResp, nil } @@ -164,7 +169,7 @@ func (c *Client) coins(ctx context.Context) (sdk.Coins, error) { return supply.Supply, nil } -func (c *Client) TxOperationsAndSignersAccountIdentifiers(signed bool, txBytes []byte) (ops []*types.Operation, signers []*types.AccountIdentifier, err error) { +func (c *Client) TxOperationsAndSignersAccountIdentifiers(signed bool, txBytes []byte) (ops []*rosettatypes.Operation, signers []*rosettatypes.AccountIdentifier, err error) { txConfig := c.getTxConfig() rawTx, err := c.txDecoder(txBytes) if err != nil { @@ -176,49 +181,85 @@ func (c *Client) TxOperationsAndSignersAccountIdentifiers(signed bool, txBytes [ return nil, nil, err } - var accountIdentifierSigners []*types.AccountIdentifier + var accountIdentifierSigners []*rosettatypes.AccountIdentifier if signed { addrs := txBldr.GetTx().GetSigners() for _, addr := range addrs { - signer := &types.AccountIdentifier{ + signer := &rosettatypes.AccountIdentifier{ Address: addr.String(), } accountIdentifierSigners = append(accountIdentifierSigners, signer) } } - return sdkTxToOperations(txBldr.GetTx(), false, false), accountIdentifierSigners, nil + rosTx, err := c.converter.ToRosetta().Tx(txBytes, nil) + if err != nil { + return nil, nil, err + } + + return rosTx.Operations, accountIdentifierSigners, nil } -// GetTx returns a transaction given its hash -func (c *Client) GetTx(ctx context.Context, hash string) (*types.Transaction, error) { +// GetTx returns a transaction given its hash, in rosetta begin block and end block are mocked +// as transaction hashes in order to adhere to balance tracking rules +func (c *Client) GetTx(ctx context.Context, hash string) (*rosettatypes.Transaction, error) { hashBytes, err := hex.DecodeString(hash) if err != nil { return nil, crgerrs.WrapError(crgerrs.ErrCodec, fmt.Sprintf("bad tx hash: %s", err)) } - // here we check for the hash length to understand if it is a begin or endblock tx or a standard tendermint tx - switch len(hashBytes) { - case beginEndBlockTxSize: - // verify if it's end or begin block operations we're trying to query - switch hashBytes[0] { - case beginBlockHashStart: - return c.beginBlockTx(ctx, hashBytes[1:]) - case endBlockHashStart: - return c.endBlockTx(ctx, hashBytes[1:]) - default: - return nil, crgerrs.WrapError(crgerrs.ErrBadArgument, fmt.Sprintf("bad begin endblock starting byte: %x", hashBytes[0])) + // get tx type and hash + txType, hashBytes := c.converter.FromRosetta().HashToTxType(hashBytes) + + // construct rosetta tx + switch txType { + // handle begin block hash + case BeginBlockTx: + // get block height by hash + block, err := c.clientCtx.Client.BlockByHash(ctx, hashBytes) + if err != nil { + return nil, crgerrs.WrapError(crgerrs.ErrUnknown, err.Error()) + } + + // get block txs + fullBlock, err := c.blockTxs(ctx, &block.Block.Height) + if err != nil { + return nil, err + } + + return fullBlock.Transactions[0], nil + // handle deliver tx hash + case DeliverTxTx: + rawTx, err := c.clientCtx.Client.Tx(ctx, hashBytes, true) + if err != nil { + log.Printf("tx err: %s : %s", hash, err) + return nil, crgerrs.WrapError(crgerrs.ErrUnknown, err.Error()) } - // standard tx... - case sha256.Size: - return c.getTx(ctx, hashBytes) + return c.converter.ToRosetta().Tx(rawTx.Tx, &rawTx.TxResult) + // handle end block hash + case EndBlockTx: + // get block height by hash + block, err := c.clientCtx.Client.BlockByHash(ctx, hashBytes) + if err != nil { + return nil, crgerrs.WrapError(crgerrs.ErrUnknown, err.Error()) + } + + // get block txs + fullBlock, err := c.blockTxs(ctx, &block.Block.Height) + if err != nil { + return nil, err + } + + // get last tx + return fullBlock.Transactions[len(fullBlock.Transactions)-1], nil + // unrecognized tx default: - return nil, crgerrs.WrapError(crgerrs.ErrBadArgument, fmt.Sprintf("invalid tx size: %d", len(hashBytes))) + return nil, crgerrs.WrapError(crgerrs.ErrBadArgument, fmt.Sprintf("invalid tx hash provided: %s", hash)) } } // GetUnconfirmedTx gets an unconfirmed transaction given its hash -func (c *Client) GetUnconfirmedTx(ctx context.Context, hash string) (*types.Transaction, error) { +func (c *Client) GetUnconfirmedTx(ctx context.Context, hash string) (*rosettatypes.Transaction, error) { res, err := c.clientCtx.Client.UnconfirmedTxs(ctx, nil) if err != nil { return nil, crgerrs.WrapError(crgerrs.ErrNotFound, "unconfirmed tx not found") @@ -233,9 +274,9 @@ func (c *Client) GetUnconfirmedTx(ctx context.Context, hash string) (*types.Tran switch len(hashAsBytes) { default: return nil, crgerrs.WrapError(crgerrs.ErrBadArgument, fmt.Sprintf("unrecognized tx size: %d", len(hashAsBytes))) - case beginEndBlockTxSize: + case BeginEndBlockTxSize: return nil, crgerrs.WrapError(crgerrs.ErrBadArgument, fmt.Sprintf("endblock and begin block txs cannot be unconfirmed")) - case deliverTxSize: + case DeliverTxSize: break } @@ -245,43 +286,43 @@ func (c *Client) GetUnconfirmedTx(ctx context.Context, hash string) (*types.Tran continue } - return sdkTxToRosettaTx(c.txDecoder, unconfirmedTx, nil) + return c.converter.ToRosetta().Tx(unconfirmedTx, nil) } return nil, crgerrs.WrapError(crgerrs.ErrNotFound, "transaction not found in mempool: "+hash) } // Mempool returns the unconfirmed transactions in the mempool -func (c *Client) Mempool(ctx context.Context) ([]*types.TransactionIdentifier, error) { +func (c *Client) Mempool(ctx context.Context) ([]*rosettatypes.TransactionIdentifier, error) { txs, err := c.clientCtx.Client.UnconfirmedTxs(ctx, nil) if err != nil { return nil, err } - return tmTxsToRosettaTxsIdentifiers(txs.Txs), nil + return c.converter.ToRosetta().TxIdentifiers(txs.Txs), nil } // Peers gets the number of peers -func (c *Client) Peers(ctx context.Context) ([]*types.Peer, error) { +func (c *Client) Peers(ctx context.Context) ([]*rosettatypes.Peer, error) { netInfo, err := c.clientCtx.Client.NetInfo(ctx) if err != nil { return nil, crgerrs.WrapError(crgerrs.ErrUnknown, err.Error()) } - return tmPeersToRosettaPeers(netInfo.Peers), nil + return c.converter.ToRosetta().Peers(netInfo.Peers), nil } -func (c *Client) Status(ctx context.Context) (*types.SyncStatus, error) { +func (c *Client) Status(ctx context.Context) (*rosettatypes.SyncStatus, error) { status, err := c.clientCtx.Client.Status(ctx) if err != nil { return nil, crgerrs.WrapError(crgerrs.ErrUnknown, err.Error()) } - return tmStatusToRosettaSyncStatus(status), err + return c.converter.ToRosetta().StatusToSyncStatus(status), err } func (c *Client) getTxConfig() client.TxConfig { return c.clientCtx.TxConfig } -func (c *Client) PostTx(txBytes []byte) (*types.TransactionIdentifier, map[string]interface{}, error) { +func (c *Client) PostTx(txBytes []byte) (*rosettatypes.TransactionIdentifier, map[string]interface{}, error) { // sync ensures it will go through checkTx res, err := c.clientCtx.BroadcastTxSync(txBytes) if err != nil { @@ -292,7 +333,7 @@ func (c *Client) PostTx(txBytes []byte) (*types.TransactionIdentifier, map[strin return nil, nil, crgerrs.WrapError(crgerrs.ErrUnknown, fmt.Sprintf("transaction broadcast failure: (%d) %s ", res.Code, res.RawLog)) } - return &types.TransactionIdentifier{ + return &rosettatypes.TransactionIdentifier{ Hash: res.TxHash, }, map[string]interface{}{ @@ -300,50 +341,6 @@ func (c *Client) PostTx(txBytes []byte) (*types.TransactionIdentifier, map[strin }, nil } -func (c *Client) ConstructionMetadataFromOptions(ctx context.Context, options map[string]interface{}) (meta map[string]interface{}, err error) { - if len(options) == 0 { - return nil, crgerrs.ErrBadArgument - } - - addr, ok := options[OptionAddress] - if !ok { - return nil, crgerrs.WrapError(crgerrs.ErrInvalidAddress, "no address provided") - } - - addrString, ok := addr.(string) - if !ok { - return nil, crgerrs.WrapError(crgerrs.ErrInvalidAddress, "address is not a string") - } - - accountInfo, err := c.accountInfo(ctx, addrString, nil) - if err != nil { - return nil, err - } - - gas, ok := options[OptionGas] - if !ok { - return nil, crgerrs.WrapError(crgerrs.ErrInvalidAddress, "gas not set") - } - - memo, ok := options[OptionMemo] - if !ok { - return nil, crgerrs.WrapError(crgerrs.ErrInvalidMemo, "memo not set") - } - - status, err := c.clientCtx.Client.Status(ctx) - if err != nil { - return nil, err - } - - return map[string]interface{}{ - OptionAccountNumber: accountInfo.GetAccountNumber(), - OptionSequence: accountInfo.GetSequence(), - OptionChainID: status.NodeInfo.Network, - OptionGas: gas, - OptionMemo: memo, - }, nil -} - func (c *Client) Ready() error { ctx, cancel := context.WithTimeout(context.Background(), defaultNodeTimeout) defer cancel() @@ -394,3 +391,102 @@ func (c *Client) Bootstrap() error { return nil } + +// construction endpoints + +// ConstructionMetadataFromOptions builds the metadata given the options +func (c *Client) ConstructionMetadataFromOptions(ctx context.Context, options map[string]interface{}) (meta map[string]interface{}, err error) { + if len(options) == 0 { + return nil, crgerrs.ErrBadArgument + } + + constructionOptions := new(PreprocessOperationsOptionsResponse) + + err = constructionOptions.FromMetadata(options) + if err != nil { + return nil, err + } + + signersData := make([]*SignerData, len(constructionOptions.ExpectedSigners)) + + for i, signer := range constructionOptions.ExpectedSigners { + accountInfo, err := c.accountInfo(ctx, signer, nil) + if err != nil { + return nil, err + } + + signersData[i] = &SignerData{ + AccountNumber: accountInfo.GetAccountNumber(), + Sequence: accountInfo.GetSequence(), + } + } + + status, err := c.clientCtx.Client.Status(ctx) + if err != nil { + return nil, err + } + + metadataResp := ConstructionMetadata{ + ChainID: status.NodeInfo.Network, + SignersData: signersData, + GasLimit: constructionOptions.GasLimit, + GasPrice: constructionOptions.GasPrice, + Memo: constructionOptions.Memo, + } + + return metadataResp.ToMetadata() +} + +func (c *Client) blockTxs(ctx context.Context, height *int64) (crgtypes.BlockTransactionsResponse, error) { + // get block info + blockInfo, err := c.clientCtx.Client.Block(ctx, height) + if err != nil { + return crgtypes.BlockTransactionsResponse{}, err + } + // get block events + blockResults, err := c.clientCtx.Client.BlockResults(ctx, height) + if err != nil { + return crgtypes.BlockTransactionsResponse{}, err + } + + if len(blockResults.TxsResults) != len(blockInfo.Block.Txs) { + // wtf? + panic("block results transactions do now match block transactions") + } + // process begin and end block txs + beginBlockTx := &rosettatypes.Transaction{ + TransactionIdentifier: &rosettatypes.TransactionIdentifier{Hash: c.converter.ToRosetta().BeginBlockTxHash(blockInfo.BlockID.Hash)}, + Operations: AddOperationIndexes( + nil, + c.converter.ToRosetta().EventsToBalanceOps(StatusTxSuccess, blockResults.BeginBlockEvents), + ), + } + + endBlockTx := &rosettatypes.Transaction{ + TransactionIdentifier: &rosettatypes.TransactionIdentifier{Hash: c.converter.ToRosetta().EndBlockTxHash(blockInfo.BlockID.Hash)}, + Operations: AddOperationIndexes( + nil, + c.converter.ToRosetta().EventsToBalanceOps(StatusTxSuccess, blockResults.EndBlockEvents), + ), + } + + deliverTx := make([]*rosettatypes.Transaction, len(blockInfo.Block.Txs)) + // process normal txs + for i, tx := range blockInfo.Block.Txs { + rosTx, err := c.converter.ToRosetta().Tx(tx, blockResults.TxsResults[i]) + if err != nil { + return crgtypes.BlockTransactionsResponse{}, err + } + deliverTx[i] = rosTx + } + + finalTxs := make([]*rosettatypes.Transaction, 0, 2+len(deliverTx)) + finalTxs = append(finalTxs, beginBlockTx) + finalTxs = append(finalTxs, deliverTx...) + finalTxs = append(finalTxs, endBlockTx) + + return crgtypes.BlockTransactionsResponse{ + BlockResponse: c.converter.ToRosetta().BlockResponse(blockInfo), + Transactions: finalTxs, + }, nil +} diff --git a/server/rosetta/client_v2.go b/server/rosetta/client_v2.go deleted file mode 100644 index 522dc6029f6e..000000000000 --- a/server/rosetta/client_v2.go +++ /dev/null @@ -1,115 +0,0 @@ -package rosetta - -import ( - "context" - "crypto/sha256" - - crgtypes "github.com/tendermint/cosmos-rosetta-gateway/types" - - rosettatypes "github.com/coinbase/rosetta-sdk-go/types" - - crgerrs "github.com/tendermint/cosmos-rosetta-gateway/errors" -) - -const ( - deliverTxSize = sha256.Size - beginEndBlockTxSize = deliverTxSize + 1 - endBlockHashStart = 0x0 - beginBlockHashStart = 0x1 - burnerAddressIdentifier = "burner" -) - -func (c *Client) getTx(ctx context.Context, txHash []byte) (*rosettatypes.Transaction, error) { - rawTx, err := c.clientCtx.Client.Tx(ctx, txHash, true) - if err != nil { - return nil, crgerrs.WrapError(crgerrs.ErrUnknown, err.Error()) - } - return sdkTxToRosettaTx(c.clientCtx.TxConfig.TxDecoder(), rawTx.Tx, &rawTx.TxResult) -} - -func (c *Client) beginBlockTx(ctx context.Context, blockHash []byte) (*rosettatypes.Transaction, error) { - // get block height by hash - block, err := c.clientCtx.Client.BlockByHash(ctx, blockHash) - if err != nil { - return nil, crgerrs.WrapError(crgerrs.ErrUnknown, err.Error()) - } - - // get block txs - fullBlock, err := c.blockTxs(ctx, &block.Block.Height) - if err != nil { - return nil, err - } - - return fullBlock.Transactions[0], nil -} - -func (c *Client) endBlockTx(ctx context.Context, blockHash []byte) (*rosettatypes.Transaction, error) { - // get block height by hash - block, err := c.clientCtx.Client.BlockByHash(ctx, blockHash) - if err != nil { - return nil, crgerrs.WrapError(crgerrs.ErrUnknown, err.Error()) - } - - // get block txs - fullBlock, err := c.blockTxs(ctx, &block.Block.Height) - if err != nil { - return nil, err - } - - // get last tx - return fullBlock.Transactions[len(fullBlock.Transactions)-1], nil -} - -func (c *Client) blockTxs(ctx context.Context, height *int64) (crgtypes.BlockTransactionsResponse, error) { - // get block info - blockInfo, err := c.clientCtx.Client.Block(ctx, height) - if err != nil { - return crgtypes.BlockTransactionsResponse{}, err - } - // get block events - blockResults, err := c.clientCtx.Client.BlockResults(ctx, height) - if err != nil { - return crgtypes.BlockTransactionsResponse{}, err - } - - if len(blockResults.TxsResults) != len(blockInfo.Block.Txs) { - // wtf? - panic("block results transactions do now match block transactions") - } - // process begin and end block txs - beginBlockTx := &rosettatypes.Transaction{ - TransactionIdentifier: &rosettatypes.TransactionIdentifier{Hash: beginBlockTxHash(blockInfo.BlockID.Hash)}, - Operations: normalizeOperationIndexes( - nil, - sdkEventsToBalanceOperations(StatusTxSuccess, blockResults.BeginBlockEvents), - ), - } - - endBlockTx := &rosettatypes.Transaction{ - TransactionIdentifier: &rosettatypes.TransactionIdentifier{Hash: endBlockTxHash(blockInfo.BlockID.Hash)}, - Operations: normalizeOperationIndexes( - nil, - sdkEventsToBalanceOperations(StatusTxSuccess, blockResults.EndBlockEvents), - ), - } - - deliverTx := make([]*rosettatypes.Transaction, len(blockInfo.Block.Txs)) - // process normal txs - for i, tx := range blockInfo.Block.Txs { - rosTx, err := sdkTxToRosettaTx(c.clientCtx.TxConfig.TxDecoder(), tx, blockResults.TxsResults[i]) - if err != nil { - return crgtypes.BlockTransactionsResponse{}, err - } - deliverTx[i] = rosTx - } - - finalTxs := make([]*rosettatypes.Transaction, 0, 2+len(deliverTx)) - finalTxs = append(finalTxs, beginBlockTx) - finalTxs = append(finalTxs, deliverTx...) - finalTxs = append(finalTxs, endBlockTx) - - return crgtypes.BlockTransactionsResponse{ - BlockResponse: tmResultBlockToRosettaBlockResponse(blockInfo), - Transactions: finalTxs, - }, nil -} diff --git a/server/rosetta/client_v2_test.go b/server/rosetta/client_v2_test.go index 477ff1a236b2..1e0c0c689bea 100644 --- a/server/rosetta/client_v2_test.go +++ b/server/rosetta/client_v2_test.go @@ -2,6 +2,8 @@ package rosetta import ( "context" + "encoding/hex" + "encoding/json" "testing" ) @@ -46,3 +48,35 @@ func TestClientV2(t *testing.T) { } } } + +func TestT(t *testing.T) { + + const addr = "FD41B1F4CA2222B503F448C481873FA91BAE8492495251602A4E60A73C38F944" + cdc, ir := MakeCodec() + c, err := NewClient(&Config{ + Blockchain: "", + Network: "", + TendermintRPC: "tcp://localhost:26657", + GRPCEndpoint: "localhost:9090", + Addr: "", + Retries: 0, + Offline: false, + Codec: cdc, + InterfaceRegistry: ir, + }) + if err != nil { + t.Fatal(err) + } + c.Bootstrap() + hashBy, err := hex.DecodeString(addr) + tx, err := c.clientCtx.Client.Tx(context.Background(), hashBy, true) + t.Logf("%#v", tx.TxResult) + + // ros resp + rosTx, err := c.GetTx(context.TODO(), addr) + if err != nil { + t.Fatal(err) + } + x, err := json.Marshal(rosTx) + t.Logf("%s", x) +} diff --git a/server/rosetta/conv_from_rosetta.go b/server/rosetta/conv_from_rosetta.go deleted file mode 100644 index 0c7f851ee8fe..000000000000 --- a/server/rosetta/conv_from_rosetta.go +++ /dev/null @@ -1,108 +0,0 @@ -package rosetta - -import ( - "github.com/coinbase/rosetta-sdk-go/types" - - sdk "github.com/cosmos/cosmos-sdk/types" -) - -// sdkCoinsToRosettaAmounts converts []sdk.Coin to rosetta amounts -func sdkCoinsToRosettaAmounts(ownedCoins []sdk.Coin, availableCoins sdk.Coins) []*types.Amount { - amounts := make([]*types.Amount, len(availableCoins)) - ownedCoinsMap := make(map[string]sdk.Int, len(availableCoins)) - - for _, ownedCoin := range ownedCoins { - ownedCoinsMap[ownedCoin.Denom] = ownedCoin.Amount - } - - for i, coin := range availableCoins { - value, owned := ownedCoinsMap[coin.Denom] - if !owned { - amounts[i] = &types.Amount{ - Value: sdk.NewInt(0).String(), - Currency: &types.Currency{ - Symbol: coin.Denom, - }, - } - continue - } - amounts[i] = &types.Amount{ - Value: value.String(), - Currency: &types.Currency{ - Symbol: coin.Denom, - }, - } - } - - return amounts -} - -// sdkTxToOperations converts an sdk.Tx to rosetta operations -func sdkTxToOperations(tx sdk.Tx, withStatus, hasError bool) []*types.Operation { - var operations []*types.Operation - - msgOps := sdkMsgsToRosettaOperations(tx.GetMsgs(), withStatus, hasError) - operations = append(operations, msgOps...) - - feeTx := tx.(sdk.FeeTx) - feeOps := sdkFeeTxToOperations(feeTx, withStatus, len(msgOps)) - operations = append(operations, feeOps...) - - return operations -} - -// sdkFeeTxToOperations converts sdk.FeeTx to rosetta operations -func sdkFeeTxToOperations(feeTx sdk.FeeTx, withStatus bool, previousOps int) []*types.Operation { - feeCoins := feeTx.GetFee() - var ops []*types.Operation - if feeCoins != nil { - var feeOps = rosettaFeeOperationsFromCoins(feeCoins, feeTx.FeePayer().String(), withStatus, previousOps) - ops = append(ops, feeOps...) - } - - return ops -} - -// rosettaFeeOperationsFromCoins returns the list of rosetta fee operations given sdk coins -func rosettaFeeOperationsFromCoins(coins sdk.Coins, account string, withStatus bool, previousOps int) []*types.Operation { - feeOps := make([]*types.Operation, 0) - var status string - if withStatus { - status = StatusTxSuccess - } - - for i, coin := range coins { - op := &types.Operation{ - OperationIdentifier: &types.OperationIdentifier{ - Index: int64(previousOps + i), - }, - Type: OperationFee, - Status: status, - Account: &types.AccountIdentifier{ - Address: account, - }, - Amount: &types.Amount{ - Value: "-" + coin.Amount.String(), - Currency: &types.Currency{ - Symbol: coin.Denom, - }, - }, - } - - feeOps = append(feeOps, op) - } - - return feeOps -} - -// sdkMsgsToRosettaOperations converts sdk messages to rosetta operations -func sdkMsgsToRosettaOperations(msgs []sdk.Msg, withStatus bool, hasError bool) []*types.Operation { - var operations []*types.Operation - for _, msg := range msgs { - if rosettaMsg, ok := msg.(Msg); ok { - operations = append(operations, rosettaMsg.ToOperations(withStatus, hasError)...) - } - } - - return operations -} diff --git a/server/rosetta/conv_to_rosetta.go b/server/rosetta/conv_to_rosetta.go deleted file mode 100644 index 3a1dc004ee25..000000000000 --- a/server/rosetta/conv_to_rosetta.go +++ /dev/null @@ -1,456 +0,0 @@ -package rosetta - -import ( - "encoding/json" - "fmt" - "strconv" - "strings" - - "github.com/gogo/protobuf/proto" - crgerrs "github.com/tendermint/cosmos-rosetta-gateway/errors" - crgtypes "github.com/tendermint/cosmos-rosetta-gateway/types" - abci "github.com/tendermint/tendermint/abci/types" - tmcoretypes "github.com/tendermint/tendermint/rpc/core/types" - tmtypes "github.com/tendermint/tendermint/types" - - banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" - - "github.com/gogo/protobuf/jsonpb" - - rosettatypes "github.com/coinbase/rosetta-sdk-go/types" - - sdk "github.com/cosmos/cosmos-sdk/types" -) - -// ----------------------- sdk to rosetta ------------------------ - -// sdkTxToRosettaTx converts a tendermint raw transaction and its result (if provided) to a rosetta transaction -// using the sdk transaction decoder -func sdkTxToRosettaTx(txDecoder sdk.TxDecoder, rawTx tmtypes.Tx, txResult *abci.ResponseDeliverTx) ( - *rosettatypes.Transaction, - error) { - // decode tx - tx, err := txDecoder(rawTx) - if err != nil { - return nil, crgerrs.WrapError(crgerrs.ErrCodec, err.Error()) - } - // get initial status, as per sdk design, if one msg fails - // the whole TX will be considered failing, so we can't have - // 1 msg being success and 1 msg being reverted - status := StatusTxSuccess - switch txResult { - // if nil, we're probably checking an unconfirmed tx - case nil: - status = StatusTxUnconfirmed - // set the status - default: - if txResult.Code != abci.CodeTypeOK { - status = StatusTxReverted - } - } - // get operations from msgs - msgs := tx.GetMsgs() - var rawTxOps []*rosettatypes.Operation - for _, msg := range msgs { - ops, err := sdkMsgToRosettaOperation(status, msg) - if err != nil { - return nil, err - } - rawTxOps = append(rawTxOps, ops...) - } - - // now get balance events from response deliver tx - var balanceOps []*rosettatypes.Operation - // tx result might be nil, in case we're querying an unconfirmed tx from the mempool - if txResult != nil { - balanceOps = sdkEventsToBalanceOperations(status, txResult.Events) - } - - // now normalize indexes - totalOps := normalizeOperationIndexes(rawTxOps, balanceOps) - - return &rosettatypes.Transaction{ - TransactionIdentifier: &rosettatypes.TransactionIdentifier{Hash: fmt.Sprintf("%x", rawTx.Hash())}, - Operations: totalOps, - }, nil -} - -// sdkEventsToBalanceOperations takes a slice of tendermint events and converts them to -// balance operations -func sdkEventsToBalanceOperations(status string, events []abci.Event) []*rosettatypes.Operation { - var ops []*rosettatypes.Operation - - for _, e := range events { - balanceOps, ok := sdkEventToBalanceOperations(status, e) - if !ok { - continue - } - ops = append(ops, balanceOps...) - } - - return ops -} - -// sdkEventToBalanceOperations converts an event to a rosetta balance operation -// it will panic if the event is malformed because it might mean the sdk spec -// has changed and rosetta needs to reflect those changes too. -// The balance operations are multiple, one for each denom. -func sdkEventToBalanceOperations(status string, event abci.Event) (operations []*rosettatypes.Operation, isBalanceEvent bool) { - - var ( - accountIdentifier string - coinChange sdk.Coins - isSub bool - ) - - switch event.Type { - default: - return nil, false - case banktypes.EventTypeCoinSpent: - spender, err := sdk.AccAddressFromBech32((string)(event.Attributes[0].Value)) - if err != nil { - panic(err) - } - coins, err := sdk.ParseCoinsNormalized((string)(event.Attributes[1].Value)) - if err != nil { - panic(err) - } - - isSub = true - coinChange = coins - accountIdentifier = spender.String() - - case banktypes.EventTypeCoinReceived: - receiver, err := sdk.AccAddressFromBech32((string)(event.Attributes[0].Value)) - if err != nil { - panic(err) - } - coins, err := sdk.ParseCoinsNormalized((string)(event.Attributes[1].Value)) - if err != nil { - panic(err) - } - - isSub = false - coinChange = coins - accountIdentifier = receiver.String() - - // rosetta does not have the concept of burning coins, so we need to mock - // the burn as a send to an address that cannot be resolved to anything - case banktypes.EventTypeCoinBurn: - coins, err := sdk.ParseCoinsNormalized((string)(event.Attributes[1].Value)) - if err != nil { - panic(err) - } - - coinChange = coins - accountIdentifier = burnerAddressIdentifier - } - - operations = make([]*rosettatypes.Operation, len(coinChange)) - - for i, coin := range coinChange { - - value := coin.Amount.String() - // in case the event is a subtract balance one the rewrite value with - // the negative coin identifier - if isSub { - value = "-" + value - } - - op := &rosettatypes.Operation{ - Type: event.Type, - Status: status, - Account: &rosettatypes.AccountIdentifier{Address: accountIdentifier}, - Amount: &rosettatypes.Amount{ - Value: value, - Currency: &rosettatypes.Currency{ - Symbol: coin.Denom, - Decimals: 0, - }, - }, - } - - operations[i] = op - } - return operations, true -} - -// sdkMsgToRosettaOperation will create an operation for each msg signer -// with the message proto name as type, and the raw fields -// as metadata -func sdkMsgToRosettaOperation(status string, msg sdk.Msg) ([]*rosettatypes.Operation, error) { - opName := proto.MessageName(msg) - // in case proto does not recognize the message name - // then we should try to cast it to service msg, to - // check if it was wrapped or not, in case the cast - // from sdk.ServiceMsg to sdk.Msg fails, then a - // codec error is returned - if opName == "" { - unwrappedMsg, ok := msg.(sdk.ServiceMsg) - if !ok { - return nil, crgerrs.WrapError(crgerrs.ErrCodec, fmt.Sprintf("unrecognized message type: %T", msg)) - } - - msg, ok = unwrappedMsg.Request.(sdk.Msg) - if !ok { - return nil, crgerrs.WrapError( - crgerrs.ErrCodec, - fmt.Sprintf("unable to cast %T to sdk.Msg, method: %s", unwrappedMsg, unwrappedMsg.MethodName), - ) - } - - opName = proto.MessageName(msg) - if opName == "" { - return nil, crgerrs.WrapError(crgerrs.ErrCodec, fmt.Sprintf("unrecognized message type: %T", msg)) - } - } - - meta, err := sdkMsgToRosettaMetadata(msg) - if err != nil { - return nil, err - } - - ops := make([]*rosettatypes.Operation, len(msg.GetSigners())) - for i, signer := range msg.GetSigners() { - op := &rosettatypes.Operation{ - Type: opName, - Status: status, - Account: &rosettatypes.AccountIdentifier{Address: signer.String()}, - Metadata: meta, - } - - ops[i] = op - } - - return ops, nil -} - -// sdkMsgToRosettaMetadata converts an sdk.Msg to map[string]interface{} -func sdkMsgToRosettaMetadata(msg sdk.Msg) (map[string]interface{}, error) { - b, err := json.Marshal(msg) - if err != nil { - return nil, crgerrs.WrapError(crgerrs.ErrCodec, err.Error()) - } - m := make(map[string]interface{}) - err = json.Unmarshal(b, &m) - if err != nil { - return nil, crgerrs.WrapError(crgerrs.ErrCodec, err.Error()) - } - return m, nil -} - -// ---------------------------- end ------------------------------ - -// opsToMsgsAndFees converts rosetta operations to sdk.Msg and fees represented as sdk.Coins -func opsToMsgsAndFees(interfaceRegistry jsonpb.AnyResolver, ops []*rosettatypes.Operation) ([]sdk.Msg, sdk.Coins, error) { - var feeAmnt []*rosettatypes.Amount - var newOps []*rosettatypes.Operation - var msgType string - // find the fee operation and put it aside - for _, op := range ops { - switch op.Type { - case OperationFee: - amount := op.Amount - feeAmnt = append(feeAmnt, amount) - default: - // check if operation matches the one already used - // as, at the moment, we only support operations - // that represent a single cosmos-sdk message - switch { - // if msgType was not set then set it - case msgType == "": - msgType = op.Type - // if msgType does not match op.Type then it means we're trying to send multiple messages in a single tx - case msgType != op.Type: - return nil, nil, fmt.Errorf("only single message operations are supported: %s - %s", msgType, op.Type) - } - // append operation to new ops list - newOps = append(newOps, op) - } - } - // convert all operations, except fee op to sdk.Msgs - msgs, err := opsToMsgs(interfaceRegistry, newOps) - if err != nil { - return nil, nil, err - } - - return msgs, amountsToCoins(feeAmnt), nil -} - -// amountsToCoins converts rosetta amounts to sdk coins -func amountsToCoins(amounts []*rosettatypes.Amount) sdk.Coins { - var feeCoins sdk.Coins - - for _, amount := range amounts { - absValue := strings.Trim(amount.Value, "-") - value, err := strconv.ParseInt(absValue, 10, 64) - if err != nil { - return nil - } - coin := sdk.NewCoin(amount.Currency.Symbol, sdk.NewInt(value)) - feeCoins = append(feeCoins, coin) - } - - return feeCoins -} - -func opsToMsgs(interfaceRegistry jsonpb.AnyResolver, ops []*rosettatypes.Operation) ([]sdk.Msg, error) { - var msgs []sdk.Msg - var operationsByType = make(map[string][]*rosettatypes.Operation) - for _, op := range ops { - operationsByType[op.Type] = append(operationsByType[op.Type], op) - } - - for opName, operations := range operationsByType { - if opName == OperationFee { - continue - } - - msgType, err := interfaceRegistry.Resolve("/" + opName) // Types are registered as /proto-name in the interface registry. - if err != nil { - return nil, err - } - - if rosettaMsg, ok := msgType.(Msg); ok { - m, err := rosettaMsg.FromOperations(operations) - if err != nil { - return nil, err - } - msgs = append(msgs, m) - } - } - - return msgs, nil -} - -// ------------------- from tendermint to rosetta ------------------ - -// tmTxsToRosettaTxsIdentifiers converts a tendermint raw transactions into an array of rosetta tx identifiers -func tmTxsToRosettaTxsIdentifiers(txs []tmtypes.Tx) []*rosettatypes.TransactionIdentifier { - converted := make([]*rosettatypes.TransactionIdentifier, len(txs)) - for i, tx := range txs { - converted[i] = tmTxToRosettaTxIdentifier(tx) - } - - return converted -} - -// tmTxToRosettaTxIdentifier converts a tendermint raw transaction into a rosetta tx identifier -func tmTxToRosettaTxIdentifier(tx tmtypes.Tx) *rosettatypes.TransactionIdentifier { - return &rosettatypes.TransactionIdentifier{Hash: fmt.Sprintf("%x", tx.Hash())} -} - -// tmPeersToRosettaPeers converts tendermint peers to rosetta ones -func tmPeersToRosettaPeers(peers []tmcoretypes.Peer) []*rosettatypes.Peer { - converted := make([]*rosettatypes.Peer, len(peers)) - - for i, peer := range peers { - converted[i] = &rosettatypes.Peer{ - PeerID: peer.NodeInfo.Moniker, - Metadata: map[string]interface{}{ - "addr": peer.NodeInfo.ListenAddr, - }, - } - } - - return converted -} - -// tmStatusToRosettaSyncStatus converts a tendermint status to rosetta sync status -func tmStatusToRosettaSyncStatus(status *tmcoretypes.ResultStatus) *rosettatypes.SyncStatus { - // determine sync status - var stage = StatusPeerSynced - if status.SyncInfo.CatchingUp { - stage = StatusPeerSyncing - } - - return &rosettatypes.SyncStatus{ - CurrentIndex: status.SyncInfo.LatestBlockHeight, - TargetIndex: nil, // sync info does not allow us to get target height - Stage: &stage, - } -} - -// tmBlockToRosettaBlockIdentifier converts a tendermint result block to a rosetta block identifier -func tmBlockToRosettaBlockIdentifier(block *tmcoretypes.ResultBlock) *rosettatypes.BlockIdentifier { - return &rosettatypes.BlockIdentifier{ - Index: block.Block.Height, - Hash: block.Block.Hash().String(), - } -} - -// tmBlockToRosettaParentBlockIdentifier returns the parent block identifier from the last block -func tmBlockToRosettaParentBlockIdentifier(block *tmcoretypes.ResultBlock) *rosettatypes.BlockIdentifier { - if block.Block.Height == 1 { - return &rosettatypes.BlockIdentifier{ - Index: 1, - Hash: fmt.Sprintf("%X", block.BlockID.Hash.Bytes()), - } - } - - return &rosettatypes.BlockIdentifier{ - Index: block.Block.Height - 1, - Hash: fmt.Sprintf("%X", block.Block.LastBlockID.Hash.Bytes()), - } -} - -// tmResultBlockToRosettaBlockResponse converts a tendermint result block to block response -func tmResultBlockToRosettaBlockResponse(block *tmcoretypes.ResultBlock) crgtypes.BlockResponse { - return crgtypes.BlockResponse{ - Block: tmBlockToRosettaBlockIdentifier(block), - ParentBlock: tmBlockToRosettaParentBlockIdentifier(block), - MillisecondTimestamp: timeToMilliseconds(block.Block.Time), - TxCount: int64(len(block.Block.Txs)), - } -} - -// --------------------------- end --------------------------------- - -// ----------------------- raw ros utils --------------------------- - -// endBlockTxHash produces a mock endblock hash that rosetta can query -// for endblock operations, it also serves the purpose of representing -// part of the state changes happening at endblock level (balance ones) -func endBlockTxHash(hash []byte) string { - return fmt.Sprintf("%x%x", endBlockHashStart, hash) -} - -// beginBlockTxHash produces a mock beginblock hash that rosetta can query -// for beginblock operations, it also serves the purpose of representing -// part of the state changes happening at beginblock level (balance ones) -func beginBlockTxHash(hash []byte) string { - return fmt.Sprintf("%x%x", beginBlockHashStart, hash) -} - -// normalizeOperationIndexes adds the indexes to operations adhering to specific rules: -// operations related to messages will be always before than the balance ones -func normalizeOperationIndexes(msgOps []*rosettatypes.Operation, balanceOps []*rosettatypes.Operation) (finalOps []*rosettatypes.Operation) { - lenMsgOps := len(msgOps) - lenBalanceOps := len(balanceOps) - finalOps = make([]*rosettatypes.Operation, 0, lenMsgOps+lenBalanceOps) - - var currentIndex int64 - // add indexes to msg ops - for _, op := range msgOps { - op.OperationIdentifier = &rosettatypes.OperationIdentifier{ - Index: currentIndex, - } - - finalOps = append(finalOps, op) - currentIndex++ - } - - // add indexes to balance ops - for _, op := range balanceOps { - op.OperationIdentifier = &rosettatypes.OperationIdentifier{ - Index: currentIndex, - } - - finalOps = append(finalOps, op) - currentIndex++ - } - - return finalOps -} - -// --------------------------- end --------------------------------- diff --git a/server/rosetta/convert/converter.go b/server/rosetta/converter.go similarity index 73% rename from server/rosetta/convert/converter.go rename to server/rosetta/converter.go index ee0b6f3d0276..f18e566f24ed 100644 --- a/server/rosetta/convert/converter.go +++ b/server/rosetta/converter.go @@ -1,8 +1,10 @@ -package convert +package rosetta import ( "encoding/json" "fmt" + crgtypes "github.com/tendermint/cosmos-rosetta-gateway/types" + tmcoretypes "github.com/tendermint/tendermint/rpc/core/types" "reflect" rosettatypes "github.com/coinbase/rosetta-sdk-go/types" @@ -14,7 +16,6 @@ import ( sdkclient "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/codec" codectypes "github.com/cosmos/cosmos-sdk/codec/types" - "github.com/cosmos/cosmos-sdk/server/rosetta/types" sdk "github.com/cosmos/cosmos-sdk/types" authsigning "github.com/cosmos/cosmos-sdk/x/auth/signing" banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" @@ -31,6 +32,12 @@ type Converter interface { // all the functions used to convert sdk and // tendermint types to rosetta known types type ToRosettaConverter interface { + // BlockResponse returns a block response given a result block + BlockResponse(block *tmcoretypes.ResultBlock) crgtypes.BlockResponse + // BeginBlockToTx converts the given begin block hash to rosetta transaction hash + BeginBlockTxHash(blockHash []byte) string + // EndBlockTxHash converts the given endblock hash to rosetta transaction hash + EndBlockTxHash(blockHash []byte) string // CoinsToAmounts converts sdk.Coins to amounts CoinsToAmounts(ownedCoins []sdk.Coin, availableCoins sdk.Coins) []*rosettatypes.Amount // MsgToOps converts an sdk.Msg to a rosetta operation @@ -39,8 +46,14 @@ type ToRosettaConverter interface { MsgToMeta(msg sdk.Msg) (meta map[string]interface{}, err error) // Tx converts a tendermint transaction and tx result if provided to a rosetta tx Tx(rawTx tmtypes.Tx, txResult *abci.ResponseDeliverTx) (*rosettatypes.Transaction, error) + // TxIdentifiers converts a tendermint tx to transaction identifiers + TxIdentifiers(txs []tmtypes.Tx) []*rosettatypes.TransactionIdentifier // EventsToBalanceOps converts events to balance operations EventsToBalanceOps(status string, events []abci.Event) []*rosettatypes.Operation + // StatusToSyncStatus converts a tendermint status to sync status + StatusToSyncStatus(status *tmcoretypes.ResultStatus) *rosettatypes.SyncStatus + // Peers converts tendermint peers to rosetta + Peers(peers []tmcoretypes.Peer) []*rosettatypes.Peer } // FromRosettaConverter is an interface that exposes @@ -49,6 +62,7 @@ type ToRosettaConverter interface { type FromRosettaConverter interface { OpsToUnsignedTx(ops []*rosettatypes.Operation) (tx authsigning.Tx, err error) MetaToMsg(meta map[string]interface{}, msg sdk.Msg) (err error) + HashToTxType(hashBytes []byte) (txType TransactionType, realHash []byte) } type converter struct { @@ -236,15 +250,17 @@ func (c converter) Tx(rawTx tmtypes.Tx, txResult *abci.ResponseDeliverTx) (*rose // get initial status, as per sdk design, if one msg fails // the whole TX will be considered failing, so we can't have // 1 msg being success and 1 msg being reverted - status := types.StatusTxSuccess + status := StatusTxSuccess switch txResult { // if nil, we're probably checking an unconfirmed tx + // or trying to build a new transaction, so status + // is not put inside case nil: - status = types.StatusTxUnconfirmed + status = "" // set the status default: if txResult.Code != abci.CodeTypeOK { - status = types.StatusTxReverted + status = StatusTxReverted } } // get operations from msgs @@ -269,7 +285,7 @@ func (c converter) Tx(rawTx tmtypes.Tx, txResult *abci.ResponseDeliverTx) (*rose totalOps := AddOperationIndexes(rawTxOps, balanceOps) return &rosettatypes.Transaction{ - TransactionIdentifier: &rosettatypes.TransactionIdentifier{Hash: fmt.Sprintf("%x", rawTx.Hash())}, + TransactionIdentifier: &rosettatypes.TransactionIdentifier{Hash: fmt.Sprintf("%X", rawTx.Hash())}, Operations: totalOps, }, nil } @@ -340,7 +356,7 @@ func sdkEventToBalanceOperations(status string, event abci.Event) (operations [] } coinChange = coins - accountIdentifier = types.BurnerAddressIdentifier + accountIdentifier = BurnerAddressIdentifier } operations = make([]*rosettatypes.Operation, len(coinChange)) @@ -433,3 +449,107 @@ func AddOperationIndexes(msgOps []*rosettatypes.Operation, balanceOps []*rosetta return finalOps } + +// endBlockTxHash produces a mock endblock hash that rosetta can query +// for endblock operations, it also serves the purpose of representing +// part of the state changes happening at endblock level (balance ones) +func (c converter) EndBlockTxHash(hash []byte) string { + return fmt.Sprintf("%X%X", EndBlockHashStart, hash) +} + +// beginBlockTxHash produces a mock beginblock hash that rosetta can query +// for beginblock operations, it also serves the purpose of representing +// part of the state changes happening at beginblock level (balance ones) +func (c converter) BeginBlockTxHash(hash []byte) string { + return fmt.Sprintf("%X%X", BeginBlockHashStart, hash) +} + +// HashToTxType takes the provided hash bytes from rosetta and discerns if they are +// a deliver tx type or endblock/begin block hash, returning the real hash afterwards +func (c converter) HashToTxType(hashBytes []byte) (txType TransactionType, realHash []byte) { + switch len(hashBytes) { + case DeliverTxSize: + return DeliverTxTx, hashBytes + + case BeginEndBlockTxSize: + switch hashBytes[0] { + case BeginBlockHashStart: + return BeginBlockTx, hashBytes[1:] + case EndBlockHashStart: + return EndBlockTx, hashBytes[1:] + default: + return UnrecognizedTx, nil + } + + default: + return UnrecognizedTx, nil + } +} + +// StatusToSyncStatus converts a tendermint status to rosetta sync status +func (c converter) StatusToSyncStatus(status *tmcoretypes.ResultStatus) *rosettatypes.SyncStatus { + // determine sync status + var stage = StatusPeerSynced + if status.SyncInfo.CatchingUp { + stage = StatusPeerSyncing + } + + return &rosettatypes.SyncStatus{ + CurrentIndex: status.SyncInfo.LatestBlockHeight, + TargetIndex: nil, // sync info does not allow us to get target height + Stage: &stage, + } +} + +// TxIdentifiers converts a tendermint raw transactions into an array of rosetta tx identifiers +func (c converter) TxIdentifiers(txs []tmtypes.Tx) []*rosettatypes.TransactionIdentifier { + converted := make([]*rosettatypes.TransactionIdentifier, len(txs)) + for i, tx := range txs { + converted[i] = &rosettatypes.TransactionIdentifier{Hash: fmt.Sprintf("%X", tx.Hash())} + } + + return converted +} + +// tmResultBlockToRosettaBlockResponse converts a tendermint result block to block response +func (c converter) BlockResponse(block *tmcoretypes.ResultBlock) crgtypes.BlockResponse { + var parentBlock *rosettatypes.BlockIdentifier + + switch block.Block.Height { + case 1: + parentBlock = &rosettatypes.BlockIdentifier{ + Index: 1, + Hash: fmt.Sprintf("%X", block.BlockID.Hash.Bytes()), + } + default: + parentBlock = &rosettatypes.BlockIdentifier{ + Index: block.Block.Height - 1, + Hash: fmt.Sprintf("%X", block.Block.LastBlockID.Hash.Bytes()), + } + } + return crgtypes.BlockResponse{ + Block: &rosettatypes.BlockIdentifier{ + Index: block.Block.Height, + Hash: block.Block.Hash().String(), + }, + ParentBlock: parentBlock, + MillisecondTimestamp: timeToMilliseconds(block.Block.Time), + TxCount: int64(len(block.Block.Txs)), + } +} + +// Peers converts tm peers to rosetta peers +func (c converter) Peers(peers []tmcoretypes.Peer) []*rosettatypes.Peer { + converted := make([]*rosettatypes.Peer, len(peers)) + + for i, peer := range peers { + converted[i] = &rosettatypes.Peer{ + PeerID: peer.NodeInfo.Moniker, + Metadata: map[string]interface{}{ + "addr": peer.NodeInfo.ListenAddr, + }, + } + } + + return converted +} diff --git a/server/rosetta/convert/converter_test.go b/server/rosetta/converter_test.go similarity index 86% rename from server/rosetta/convert/converter_test.go rename to server/rosetta/converter_test.go index 5b635d817e41..e1b599b962cc 100644 --- a/server/rosetta/convert/converter_test.go +++ b/server/rosetta/converter_test.go @@ -1,4 +1,4 @@ -package convert +package rosetta import ( "testing" @@ -7,12 +7,8 @@ import ( "github.com/stretchr/testify/suite" crgerrs "github.com/tendermint/cosmos-rosetta-gateway/errors" - "github.com/cosmos/cosmos-sdk/codec" - codectypes "github.com/cosmos/cosmos-sdk/codec/types" - cryptocodec "github.com/cosmos/cosmos-sdk/crypto/codec" sdk "github.com/cosmos/cosmos-sdk/types" authtx "github.com/cosmos/cosmos-sdk/x/auth/tx" - auth "github.com/cosmos/cosmos-sdk/x/auth/types" bank "github.com/cosmos/cosmos-sdk/x/bank/types" ) @@ -23,12 +19,7 @@ type ConverterTestSuite struct { } func (s *ConverterTestSuite) SetupTest() { - ir := codectypes.NewInterfaceRegistry() - cdc := codec.NewProtoCodec(ir) - - auth.RegisterInterfaces(ir) - bank.RegisterInterfaces(ir) - cryptocodec.RegisterInterfaces(ir) + cdc, ir := MakeCodec() txConfig := authtx.NewTxConfig(cdc, authtx.DefaultSignModes) s.c = NewConverter(cdc, ir, txConfig) } diff --git a/server/rosetta/types.go b/server/rosetta/types.go index d96afa5e679c..c92661a6176c 100644 --- a/server/rosetta/types.go +++ b/server/rosetta/types.go @@ -1,42 +1,92 @@ package rosetta import ( - "github.com/coinbase/rosetta-sdk-go/types" - - sdk "github.com/cosmos/cosmos-sdk/types" + "crypto/sha256" ) // statuses const ( - StatusTxSuccess = "Success" - StatusTxReverted = "Reverted" - StatusTxUnconfirmed = "Unconfirmed" - StatusPeerSynced = "synced" - StatusPeerSyncing = "syncing" + StatusTxSuccess = "Success" + StatusTxReverted = "Reverted" + StatusPeerSynced = "synced" + StatusPeerSyncing = "syncing" ) - -// misc const ( - Log = "log" + DeliverTxSize = sha256.Size + BeginEndBlockTxSize = DeliverTxSize + 1 + EndBlockHashStart = 0x0 + BeginBlockHashStart = 0x1 + BurnerAddressIdentifier = "burner" ) -// operations +// TransactionType is used to distinguish if a rosetta provided hash +// represents endblock, beginblock or deliver tx +type TransactionType int + const ( - OperationFee = "fee" + BeginBlockTx TransactionType = iota + EndBlockTx + DeliverTxTx + UnrecognizedTx ) -// options +// metadata options + +// misc const ( - OptionAccountNumber = "account_number" - OptionAddress = "address" - OptionChainID = "chain_id" - OptionSequence = "sequence" - OptionMemo = "memo" - OptionGas = "gas" + Log = "log" ) -type Msg interface { - sdk.Msg - ToOperations(withStatus, hasError bool) []*types.Operation - FromOperations(ops []*types.Operation) (sdk.Msg, error) +// ConstructionPreprocessMetadata is used to represent +// the metadata rosetta can provide during preprocess options +type ConstructionPreprocessMetadata struct { + Memo string `json:"memo"` + GasLimit uint64 `json:"gas_limit"` + GasPrice string `json:"gas_price"` +} + +func (c *ConstructionPreprocessMetadata) FromMetadata(meta map[string]interface{}) error { + return unmarshalMetadata(meta, c) +} + +// PreprocessOperationsOptionsResponse is the structured metadata options returned by the preprocess operations endpoint +type PreprocessOperationsOptionsResponse struct { + ExpectedSigners []string `json:"expected_signers"` + Memo string `json:"memo"` + GasLimit uint64 `json:"gas_limit"` + GasPrice string `json:"gas_price"` +} + +func (c PreprocessOperationsOptionsResponse) ToMetadata() (map[string]interface{}, error) { + return marshalMetadata(c) +} + +func (c *PreprocessOperationsOptionsResponse) FromMetadata(meta map[string]interface{}) error { + return unmarshalMetadata(meta, c) +} + +// SignerData contains information on the signers when the request +// is being created, used to populate the account information +type SignerData struct { + AccountNumber uint64 `json:"account_number"` + Sequence uint64 `json:"sequence"` +} + +// ConstructionMetadata are the metadata options used to +// construct a transaction. It is returned by ConstructionMetadataFromOptions +// and fed to ConstructionPayload to process the bytes to sign. +type ConstructionMetadata struct { + ChainID string `json:"chain_id"` + SignersData []*SignerData `json:"signer_data"` + GasLimit uint64 `json:"gas_limit"` + GasPrice string `json:"gas_price"` + Memo string `json:"memo"` +} + +func (c ConstructionMetadata) ToMetadata() (map[string]interface{}, error) { + return marshalMetadata(c) +} + +func (c *ConstructionMetadata) FromMetadata(meta map[string]interface{}) error { + return unmarshalMetadata(meta, c) } diff --git a/server/rosetta/util.go b/server/rosetta/util.go index 6e9904f06e46..42861a0991dc 100644 --- a/server/rosetta/util.go +++ b/server/rosetta/util.go @@ -1,10 +1,9 @@ package rosetta import ( - "fmt" + "encoding/json" + crgerrs "github.com/tendermint/cosmos-rosetta-gateway/errors" "time" - - "github.com/coinbase/rosetta-sdk-go/types" ) // timeToMilliseconds converts time to milliseconds timestamp @@ -12,67 +11,32 @@ func timeToMilliseconds(t time.Time) int64 { return t.UnixNano() / (int64(time.Millisecond) / int64(time.Nanosecond)) } -type PayloadReqMetadata struct { - ChainID string - Sequence uint64 - AccountNumber uint64 - Gas uint64 - Memo string -} - -// getMetadataFromPayloadReq obtains the metadata from the request to /construction/payloads endpoint. -func getMetadataFromPayloadReq(req *types.ConstructionPayloadsRequest) (*PayloadReqMetadata, error) { - chainID, ok := req.Metadata[OptionChainID].(string) - if !ok { - return nil, fmt.Errorf("chain_id metadata was not provided") - } - - sequence, ok := req.Metadata[OptionSequence] - if !ok { - return nil, fmt.Errorf("sequence metadata was not provided") - } - - seqNum, ok := sequence.(float64) - if !ok { - return nil, fmt.Errorf("invalid sequence value") +// unmarshalMetadata unmarshals the given meta to the target +func unmarshalMetadata(meta map[string]interface{}, target interface{}) error { + b, err := json.Marshal(meta) + if err != nil { + return crgerrs.WrapError(crgerrs.ErrCodec, err.Error()) } - accountNum, ok := req.Metadata[OptionAccountNumber] - if !ok { - return nil, fmt.Errorf("account_number metadata was not provided") + err = json.Unmarshal(b, target) + if err != nil { + return crgerrs.WrapError(crgerrs.ErrCodec, err.Error()) } - accNum, ok := accountNum.(float64) - if !ok { - fmt.Printf("this is type %T", accountNum) - return nil, fmt.Errorf("invalid account_number value") - } - - gasNum, ok := req.Metadata[OptionGas] - if !ok { - return nil, fmt.Errorf("gas metadata was not provided") - } - - gasF64, ok := gasNum.(float64) - if !ok { - return nil, fmt.Errorf("invalid gas value") - } + return nil +} - memo, ok := req.Metadata[OptionMemo] - if !ok { - memo = "" +// marshalMetadata marshals the given interface to map[string]interface{} +func marshalMetadata(o interface{}) (meta map[string]interface{}, err error) { + b, err := json.Marshal(o) + if err != nil { + return nil, crgerrs.WrapError(crgerrs.ErrCodec, err.Error()) } - - memoStr, ok := memo.(string) - if !ok { - return nil, fmt.Errorf("invalid memo") + meta = make(map[string]interface{}) + err = json.Unmarshal(b, &meta) + if err != nil { + return nil, err } - return &PayloadReqMetadata{ - ChainID: chainID, - Sequence: uint64(seqNum), - AccountNumber: uint64(accNum), - Gas: uint64(gasF64), - Memo: memoStr, - }, nil + return } diff --git a/x/bank/types/msgs.go b/x/bank/types/msgs.go index dd41975468ca..22fdc30b3886 100644 --- a/x/bank/types/msgs.go +++ b/x/bank/types/msgs.go @@ -1,13 +1,6 @@ package types import ( - "fmt" - "strconv" - "strings" - - "github.com/coinbase/rosetta-sdk-go/types" - "github.com/gogo/protobuf/proto" - sdk "github.com/cosmos/cosmos-sdk/types" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" ) @@ -69,83 +62,6 @@ func (msg MsgSend) GetSigners() []sdk.AccAddress { return []sdk.AccAddress{from} } -// Rosetta interface -func (msg *MsgSend) ToOperations(withStatus bool, hasError bool) []*types.Operation { - var operations []*types.Operation - - fromAddress := msg.FromAddress - toAddress := msg.ToAddress - amounts := msg.Amount - if len(amounts) == 0 { - return []*types.Operation{} - } - - coin := amounts[0] - sendOp := func(account, amount string, index int) *types.Operation { - var status string - if withStatus { - status = "Success" - if hasError { - status = "Reverted" - } - } - return &types.Operation{ - OperationIdentifier: &types.OperationIdentifier{ - Index: int64(index), - }, - Type: proto.MessageName(msg), - Status: status, - Account: &types.AccountIdentifier{ - Address: account, - }, - Amount: &types.Amount{ - Value: amount, - Currency: &types.Currency{ - Symbol: coin.Denom, - }, - }, - } - } - operations = append(operations, - sendOp(fromAddress, "-"+coin.Amount.String(), 0), - sendOp(toAddress, coin.Amount.String(), 1), - ) - - return operations -} - -func (msg MsgSend) FromOperations(ops []*types.Operation) (sdk.Msg, error) { - var ( - from, to sdk.AccAddress - sendAmt sdk.Coin - err error - ) - - for _, op := range ops { - if strings.HasPrefix(op.Amount.Value, "-") { - from, err = sdk.AccAddressFromBech32(op.Account.Address) - if err != nil { - return nil, err - } - continue - } - - to, err = sdk.AccAddressFromBech32(op.Account.Address) - if err != nil { - return nil, err - } - - amount, err := strconv.ParseInt(op.Amount.Value, 10, 64) - if err != nil { - return nil, fmt.Errorf("invalid amount") - } - - sendAmt = sdk.NewCoin(op.Amount.Currency.Symbol, sdk.NewInt(amount)) - } - - return NewMsgSend(from, to, sdk.NewCoins(sendAmt)), nil -} - var _ sdk.Msg = &MsgMultiSend{} // NewMsgMultiSend - construct arbitrary multi-in, multi-out send msg. diff --git a/x/distribution/types/msg.go b/x/distribution/types/msg.go index bb72e55a243a..8dc72081e9ac 100644 --- a/x/distribution/types/msg.go +++ b/x/distribution/types/msg.go @@ -2,12 +2,6 @@ package types import ( - "fmt" - - rosettatypes "github.com/coinbase/rosetta-sdk-go/types" - "github.com/gogo/protobuf/proto" - - "github.com/cosmos/cosmos-sdk/server/rosetta" sdk "github.com/cosmos/cosmos-sdk/types" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" ) @@ -96,50 +90,6 @@ func (msg MsgWithdrawDelegatorReward) ValidateBasic() error { return nil } -func (msg *MsgWithdrawDelegatorReward) ToOperations(withStatus, hasError bool) []*rosettatypes.Operation { - - var status string - if withStatus { - status = rosetta.StatusTxSuccess - if hasError { - status = rosetta.StatusTxReverted - } - } - - op := &rosettatypes.Operation{ - OperationIdentifier: &rosettatypes.OperationIdentifier{ - Index: 0, - }, - RelatedOperations: nil, - Type: proto.MessageName(msg), - Status: status, - Account: &rosettatypes.AccountIdentifier{ - Address: msg.DelegatorAddress, - SubAccount: &rosettatypes.SubAccountIdentifier{ - Address: msg.ValidatorAddress, - }, - }, - } - return []*rosettatypes.Operation{op} -} - -func (msg *MsgWithdrawDelegatorReward) FromOperations(ops []*rosettatypes.Operation) (sdk.Msg, error) { - if len(ops) != 1 { - return nil, fmt.Errorf("expected one operation") - } - op := ops[0] - if op.Account == nil { - return nil, fmt.Errorf("account identifier must be specified") - } - if op.Account.SubAccount == nil { - return nil, fmt.Errorf("account identifier subaccount must be specified") - } - return &MsgWithdrawDelegatorReward{ - DelegatorAddress: op.Account.Address, - ValidatorAddress: op.Account.SubAccount.Address, - }, nil -} - func NewMsgWithdrawValidatorCommission(valAddr sdk.ValAddress) *MsgWithdrawValidatorCommission { return &MsgWithdrawValidatorCommission{ ValidatorAddress: valAddr.String(), diff --git a/x/staking/types/msg.go b/x/staking/types/msg.go index 38166581e546..a633f01c3fa4 100644 --- a/x/staking/types/msg.go +++ b/x/staking/types/msg.go @@ -2,17 +2,8 @@ package types import ( "bytes" - "fmt" - "strconv" - "strings" - - "github.com/gogo/protobuf/proto" - - rosettatypes "github.com/coinbase/rosetta-sdk-go/types" - codectypes "github.com/cosmos/cosmos-sdk/codec/types" cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" - "github.com/cosmos/cosmos-sdk/server/rosetta" sdk "github.com/cosmos/cosmos-sdk/types" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" ) @@ -264,90 +255,6 @@ func (msg MsgDelegate) ValidateBasic() error { return nil } -// Rosetta Msg interface. -func (msg *MsgDelegate) ToOperations(withStatus bool, hasError bool) []*rosettatypes.Operation { - var operations []*rosettatypes.Operation - delAddr := msg.DelegatorAddress - valAddr := msg.ValidatorAddress - coin := msg.Amount - delOp := func(account *rosettatypes.AccountIdentifier, amount string, index int) *rosettatypes.Operation { - var status string - if withStatus { - status = rosetta.StatusTxSuccess - if hasError { - status = rosetta.StatusTxReverted - } - } - return &rosettatypes.Operation{ - OperationIdentifier: &rosettatypes.OperationIdentifier{ - Index: int64(index), - }, - Type: proto.MessageName(msg), - Status: status, - Account: account, - Amount: &rosettatypes.Amount{ - Value: amount, - Currency: &rosettatypes.Currency{ - Symbol: coin.Denom, - }, - }, - } - } - delAcc := &rosettatypes.AccountIdentifier{ - Address: delAddr, - } - valAcc := &rosettatypes.AccountIdentifier{ - Address: "staking_account", - SubAccount: &rosettatypes.SubAccountIdentifier{ - Address: valAddr, - }, - } - operations = append(operations, - delOp(delAcc, "-"+coin.Amount.String(), 0), - delOp(valAcc, coin.Amount.String(), 1), - ) - return operations -} - -func (msg *MsgDelegate) FromOperations(ops []*rosettatypes.Operation) (sdk.Msg, error) { - var ( - delAddr sdk.AccAddress - valAddr sdk.ValAddress - sendAmt sdk.Coin - err error - ) - - for _, op := range ops { - if strings.HasPrefix(op.Amount.Value, "-") { - if op.Account == nil { - return nil, fmt.Errorf("account identifier must be specified") - } - delAddr, err = sdk.AccAddressFromBech32(op.Account.Address) - if err != nil { - return nil, err - } - continue - } - - if op.Account.SubAccount == nil { - return nil, fmt.Errorf("account identifier subaccount must be specified") - } - valAddr, err = sdk.ValAddressFromBech32(op.Account.SubAccount.Address) - if err != nil { - return nil, err - } - - amount, err := strconv.ParseInt(op.Amount.Value, 10, 64) - if err != nil { - return nil, fmt.Errorf("invalid amount: %w", err) - } - - sendAmt = sdk.NewCoin(op.Amount.Currency.Symbol, sdk.NewInt(amount)) - } - - return NewMsgDelegate(delAddr, valAddr, sendAmt), nil -} - // NewMsgBeginRedelegate creates a new MsgBeginRedelegate instance. //nolint:interfacer func NewMsgBeginRedelegate( @@ -403,103 +310,6 @@ func (msg MsgBeginRedelegate) ValidateBasic() error { return nil } -// Rosetta Msg interface. -func (msg *MsgBeginRedelegate) ToOperations(withStatus bool, hasError bool) []*rosettatypes.Operation { - var operations []*rosettatypes.Operation - delAddr := msg.DelegatorAddress - srcValAddr := msg.ValidatorSrcAddress - destValAddr := msg.ValidatorDstAddress - coin := msg.Amount - delOp := func(account *rosettatypes.AccountIdentifier, amount string, index int) *rosettatypes.Operation { - var status string - if withStatus { - status = rosetta.StatusTxSuccess - if hasError { - status = rosetta.StatusTxReverted - } - } - return &rosettatypes.Operation{ - OperationIdentifier: &rosettatypes.OperationIdentifier{ - Index: int64(index), - }, - Type: proto.MessageName(msg), - Status: status, - Account: account, - Amount: &rosettatypes.Amount{ - Value: amount, - Currency: &rosettatypes.Currency{ - Symbol: coin.Denom, - }, - }, - } - } - srcValAcc := &rosettatypes.AccountIdentifier{ - Address: delAddr, - SubAccount: &rosettatypes.SubAccountIdentifier{ - Address: srcValAddr, - }, - } - destValAcc := &rosettatypes.AccountIdentifier{ - Address: "staking_account", - SubAccount: &rosettatypes.SubAccountIdentifier{ - Address: destValAddr, - }, - } - operations = append(operations, - delOp(srcValAcc, "-"+coin.Amount.String(), 0), - delOp(destValAcc, coin.Amount.String(), 1), - ) - return operations -} - -func (msg *MsgBeginRedelegate) FromOperations(ops []*rosettatypes.Operation) (sdk.Msg, error) { - var ( - delAddr sdk.AccAddress - srcValAddr sdk.ValAddress - destValAddr sdk.ValAddress - sendAmt sdk.Coin - err error - ) - - for _, op := range ops { - if strings.HasPrefix(op.Amount.Value, "-") { - if op.Account == nil { - return nil, fmt.Errorf("account identifier must be specified") - } - delAddr, err = sdk.AccAddressFromBech32(op.Account.Address) - if err != nil { - return nil, err - } - - if op.Account.SubAccount == nil { - return nil, fmt.Errorf("account identifier subaccount must be specified") - } - srcValAddr, err = sdk.ValAddressFromBech32(op.Account.SubAccount.Address) - if err != nil { - return nil, err - } - continue - } - - if op.Account.SubAccount == nil { - return nil, fmt.Errorf("account identifier subaccount must be specified") - } - destValAddr, err = sdk.ValAddressFromBech32(op.Account.SubAccount.Address) - if err != nil { - return nil, err - } - - amount, err := strconv.ParseInt(op.Amount.Value, 10, 64) - if err != nil { - return nil, fmt.Errorf("invalid amount: %w", err) - } - - sendAmt = sdk.NewCoin(op.Amount.Currency.Symbol, sdk.NewInt(amount)) - } - - return NewMsgBeginRedelegate(delAddr, srcValAddr, destValAddr, sendAmt), nil -} - // NewMsgUndelegate creates a new MsgUndelegate instance. //nolint:interfacer func NewMsgUndelegate(delAddr sdk.AccAddress, valAddr sdk.ValAddress, amount sdk.Coin) *MsgUndelegate { @@ -547,88 +357,3 @@ func (msg MsgUndelegate) ValidateBasic() error { return nil } - -// Rosetta Msg interface. -func (msg *MsgUndelegate) ToOperations(withStatus bool, hasError bool) []*rosettatypes.Operation { - var operations []*rosettatypes.Operation - delAddr := msg.DelegatorAddress - valAddr := msg.ValidatorAddress - coin := msg.Amount - delOp := func(account *rosettatypes.AccountIdentifier, amount string, index int) *rosettatypes.Operation { - var status string - if withStatus { - status = rosetta.StatusTxSuccess - if hasError { - status = rosetta.StatusTxReverted - } - } - return &rosettatypes.Operation{ - OperationIdentifier: &rosettatypes.OperationIdentifier{ - Index: int64(index), - }, - Type: proto.MessageName(msg), - Status: status, - Account: account, - Amount: &rosettatypes.Amount{ - Value: amount, - Currency: &rosettatypes.Currency{ - Symbol: coin.Denom, - }, - }, - } - } - delAcc := &rosettatypes.AccountIdentifier{ - Address: delAddr, - } - valAcc := &rosettatypes.AccountIdentifier{ - Address: "staking_account", - SubAccount: &rosettatypes.SubAccountIdentifier{ - Address: valAddr, - }, - } - operations = append(operations, - delOp(valAcc, "-"+coin.Amount.String(), 0), - delOp(delAcc, coin.Amount.String(), 1), - ) - return operations -} - -func (msg *MsgUndelegate) FromOperations(ops []*rosettatypes.Operation) (sdk.Msg, error) { - var ( - delAddr sdk.AccAddress - valAddr sdk.ValAddress - undelAmt sdk.Coin - err error - ) - - for _, op := range ops { - if strings.HasPrefix(op.Amount.Value, "-") { - if op.Account.SubAccount == nil { - return nil, fmt.Errorf("account identifier subaccount must be specified") - } - valAddr, err = sdk.ValAddressFromBech32(op.Account.SubAccount.Address) - if err != nil { - return nil, err - } - continue - } - - if op.Account == nil { - return nil, fmt.Errorf("account identifier must be specified") - } - - delAddr, err = sdk.AccAddressFromBech32(op.Account.Address) - if err != nil { - return nil, err - } - - amount, err := strconv.ParseInt(op.Amount.Value, 10, 64) - if err != nil { - return nil, fmt.Errorf("invalid amount") - } - - undelAmt = sdk.NewCoin(op.Amount.Currency.Symbol, sdk.NewInt(amount)) - } - - return NewMsgUndelegate(delAddr, valAddr, undelAmt), nil -} From fbdeabce4c33950539e78c6e53f7797321d9eba9 Mon Sep 17 00:00:00 2001 From: Frojdi Dymylja Date: Thu, 4 Mar 2021 17:35:33 +0100 Subject: [PATCH 06/33] change: make construction stateless --- docs/core/baseapp.md | 2 +- go.mod | 2 +- go.sum | 4 +- server/rosetta/client_offline.go | 131 ++++++++------------- server/rosetta/client_online.go | 112 ++++++++---------- server/rosetta/client_v2_test.go | 34 ------ server/rosetta/converter.go | 190 +++++++++++++++++++++++++------ server/rosetta/converter_test.go | 37 ++++-- 8 files changed, 285 insertions(+), 227 deletions(-) diff --git a/docs/core/baseapp.md b/docs/core/baseapp.md index 5d0fd115f3ef..a0021e88648c 100644 --- a/docs/core/baseapp.md +++ b/docs/core/baseapp.md @@ -104,7 +104,7 @@ Finally, a few more important parameterd: ```go func NewBaseApp( - name string, logger log.Logger, db dbm.DB, txDecoder sdk.TxDecoder, options ...func(*BaseApp), + name string, logger log.Logger, db dbm.DB, txDecode sdk.TxDecoder, options ...func(*BaseApp), ) *BaseApp { // ... diff --git a/go.mod b/go.mod index b4b725742c8d..e9b37ab8d72c 100644 --- a/go.mod +++ b/go.mod @@ -45,7 +45,7 @@ require ( github.com/spf13/viper v1.7.1 github.com/stretchr/testify v1.7.0 github.com/tendermint/btcd v0.1.1 - github.com/tendermint/cosmos-rosetta-gateway v0.3.0-rc2 + github.com/tendermint/cosmos-rosetta-gateway v0.3.0-rc2.0.20210304154332-87d6ca4410df github.com/tendermint/crypto v0.0.0-20191022145703-50d29ede1e15 github.com/tendermint/go-amino v0.16.0 github.com/tendermint/tendermint v0.34.8 diff --git a/go.sum b/go.sum index c16c943a336b..2ef36a31a726 100644 --- a/go.sum +++ b/go.sum @@ -660,8 +660,8 @@ github.com/tecbot/gorocksdb v0.0.0-20191217155057-f0fad39f321c h1:g+WoO5jjkqGAzH github.com/tecbot/gorocksdb v0.0.0-20191217155057-f0fad39f321c/go.mod h1:ahpPrc7HpcfEWDQRZEmnXMzHY03mLDYMCxeDzy46i+8= github.com/tendermint/btcd v0.1.1 h1:0VcxPfflS2zZ3RiOAHkBiFUcPvbtRj5O7zHmcJWHV7s= github.com/tendermint/btcd v0.1.1/go.mod h1:DC6/m53jtQzr/NFmMNEu0rxf18/ktVoVtMrnDD5pN+U= -github.com/tendermint/cosmos-rosetta-gateway v0.3.0-rc2 h1:crekJuQ57yIBDuKd3/dMJ00ZvOHURuv9RGJSi2hWTW4= -github.com/tendermint/cosmos-rosetta-gateway v0.3.0-rc2/go.mod h1:gBPw8WV2Erm4UGHlBRiM3zaEBst4bsuihmMCNQdgP/s= +github.com/tendermint/cosmos-rosetta-gateway v0.3.0-rc2.0.20210304154332-87d6ca4410df h1:hoMLrOS4WyyMM+Y+iWdGu94o0zzp6Q43y7v89Q1/OIw= +github.com/tendermint/cosmos-rosetta-gateway v0.3.0-rc2.0.20210304154332-87d6ca4410df/go.mod h1:gBPw8WV2Erm4UGHlBRiM3zaEBst4bsuihmMCNQdgP/s= github.com/tendermint/crypto v0.0.0-20191022145703-50d29ede1e15 h1:hqAk8riJvK4RMWx1aInLzndwxKalgi5rTqgfXxOxbEI= github.com/tendermint/crypto v0.0.0-20191022145703-50d29ede1e15/go.mod h1:z4YtwM70uOnk8h0pjJYlj3zdYwi9l03By6iAIF5j/Pk= github.com/tendermint/go-amino v0.16.0 h1:GyhmgQKvqF82e2oZeuMSp9JTN0N09emoSZlb2lyGa2E= diff --git a/server/rosetta/client_offline.go b/server/rosetta/client_offline.go index 0495a5cffed6..1fc9a46b8d38 100644 --- a/server/rosetta/client_offline.go +++ b/server/rosetta/client_offline.go @@ -3,6 +3,7 @@ package rosetta import ( "context" "encoding/hex" + "encoding/json" "log" "strings" @@ -12,11 +13,9 @@ import ( banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" - "github.com/btcsuite/btcd/btcec" "github.com/coinbase/rosetta-sdk-go/types" crgerrs "github.com/tendermint/cosmos-rosetta-gateway/errors" - "github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/tx/signing" ) @@ -59,68 +58,19 @@ func (c *Client) SupportedOperations() []string { return supportedOperations } -func (c *Client) SignedTx(ctx context.Context, txBytes []byte, signatures []*types.Signature) (signedTxBytes []byte, err error) { - TxConfig := c.getTxConfig() - rawTx, err := TxConfig.TxDecoder()(txBytes) - if err != nil { - return nil, err - } - - txBldr, err := TxConfig.WrapTxBuilder(rawTx) - if err != nil { - return nil, err - } - - var sigs = make([]signing.SignatureV2, len(signatures)) - for i, signature := range signatures { - if signature.PublicKey.CurveType != types.Secp256k1 { - return nil, crgerrs.ErrUnsupportedCurve - } - - cmp, err := btcec.ParsePubKey(signature.PublicKey.Bytes, btcec.S256()) - if err != nil { - return nil, err - } - - compressedPublicKey := make([]byte, secp256k1.PubKeySize) - copy(compressedPublicKey, cmp.SerializeCompressed()) - pubKey := &secp256k1.PubKey{Key: compressedPublicKey} - - accountInfo, err := c.accountInfo(ctx, sdk.AccAddress(pubKey.Address()).String(), nil) - if err != nil { - return nil, err - } - - sig := signing.SignatureV2{ - PubKey: pubKey, - Data: &signing.SingleSignatureData{ - SignMode: signing.SignMode_SIGN_MODE_LEGACY_AMINO_JSON, - Signature: signature.Bytes, - }, - Sequence: accountInfo.GetSequence(), - } - sigs[i] = sig - } - - if err = txBldr.SetSignatures(sigs...); err != nil { - return nil, err - } - - txBytes, err = c.getTxConfig().TxEncoder()(txBldr.GetTx()) - if err != nil { - return nil, err - } - - return txBytes, nil +func (c *Client) SignedTx(_ context.Context, txBytes []byte, signatures []*types.Signature) (signedTxBytes []byte, err error) { + return c.converter.ToSDK().SignedTx(txBytes, signatures) } func (c *Client) ConstructionPayload(_ context.Context, request *types.ConstructionPayloadsRequest) (resp *types.ConstructionPayloadsResponse, err error) { + b, _ := json.Marshal(request) + log.Printf("raw req: %s", b) // check if there is at least one operation if len(request.Operations) < 1 { return nil, crgerrs.WrapError(crgerrs.ErrInvalidOperation, "expected at least one operation") } - tx, err := c.converter.FromRosetta().OpsToUnsignedTx(request.Operations) + tx, err := c.converter.ToSDK().UnsignedTx(request.Operations) if err != nil { return nil, crgerrs.WrapError(crgerrs.ErrInvalidOperation, err.Error()) } @@ -135,23 +85,18 @@ func (c *Client) ConstructionPayload(_ context.Context, request *types.Construct } // - builder := c.getTxConfig().NewTxBuilder() + builder := c.txConfig.NewTxBuilder() _ = builder.SetMsgs(tx.GetMsgs()...) builder.SetFeeAmount(feeAmt) builder.SetGasLimit(metadata.GasLimit) builder.SetMemo(metadata.Memo) tx = builder.GetTx() - txBytes, err := c.clientCtx.TxConfig.TxEncoder()(builder.GetTx()) - if err != nil { - return nil, err - } accIdentifiers := tx.GetSigners() - rawJSONTx, err := c.clientCtx.TxConfig.TxJSONEncoder()(builder.GetTx()) - log.Printf("raw tx: %s", rawJSONTx) payloads := make([]*types.SigningPayload, len(accIdentifiers)) + signersData := make([]signing.SignatureV2, len(accIdentifiers)) for i, accID := range accIdentifiers { // we expect pubkeys to be ordered... TODO(fdymylja): maybe make ordering not matter? signerData := authsigning.SignerData{ @@ -159,11 +104,10 @@ func (c *Client) ConstructionPayload(_ context.Context, request *types.Construct AccountNumber: metadata.SignersData[i].AccountNumber, Sequence: metadata.SignersData[i].Sequence, } - // Sign_mode_legacy_amino is being used as default here, as sign_mode_direct // needs the signer infos to be set before hand but rosetta doesn't have a way // to do this yet. To be revisited in future versions of sdk and rosetta - signBytes, err := c.clientCtx.TxConfig.SignModeHandler().GetSignBytes(signing.SignMode_SIGN_MODE_LEGACY_AMINO_JSON, signerData, tx) + signBytes, err := c.txConfig.SignModeHandler().GetSignBytes(signing.SignMode_SIGN_MODE_LEGACY_AMINO_JSON, signerData, tx) if err != nil { return nil, crgerrs.WrapError(crgerrs.ErrBadArgument, "signing error: "+err.Error()) } @@ -173,21 +117,43 @@ func (c *Client) ConstructionPayload(_ context.Context, request *types.Construct Bytes: crypto.Sha256(signBytes), SignatureType: types.Ecdsa, } + + pk, err := c.converter.ToSDK().PubKey(request.PublicKeys[i]) + if err != nil { + return nil, err + } + + signersData[i] = signing.SignatureV2{ + PubKey: pk, + Data: &signing.SingleSignatureData{}, + Sequence: metadata.SignersData[i].Sequence, + } } + // we set the signature data so we carry information regarding public key + // then afterwards we just need to set the signed bytes + err = builder.SetSignatures(signersData...) + if err != nil { + return nil, crgerrs.WrapError(crgerrs.ErrCodec, err.Error()) + } + // encode tx + encodedTx, err := c.txEncode(builder.GetTx()) + if err != nil { + return nil, crgerrs.WrapError(crgerrs.ErrCodec, err.Error()) + } return &types.ConstructionPayloadsResponse{ - UnsignedTransaction: hex.EncodeToString(txBytes), + UnsignedTransaction: hex.EncodeToString(encodedTx), Payloads: payloads, }, nil } -func (c *Client) PreprocessOperationsToOptions(_ context.Context, req *types.ConstructionPreprocessRequest) (optionsMeta map[string]interface{}, err error) { +func (c *Client) PreprocessOperationsToOptions(_ context.Context, req *types.ConstructionPreprocessRequest) (response *types.ConstructionPreprocessResponse, err error) { if len(req.Operations) == 0 { return nil, crgerrs.WrapError(crgerrs.ErrBadArgument, "no operations") } // now we need to parse the operations to cosmos sdk messages - tx, err := c.converter.FromRosetta().OpsToUnsignedTx(req.Operations) + tx, err := c.converter.ToSDK().UnsignedTx(req.Operations) if err != nil { return nil, err } @@ -195,9 +161,14 @@ func (c *Client) PreprocessOperationsToOptions(_ context.Context, req *types.Con // get the signers signers := tx.GetSigners() signersStr := make([]string, len(signers)) + accountIdentifiers := make([]*types.AccountIdentifier, len(signers)) for i, sig := range signers { - signersStr[i] = sig.String() + addr := sig.String() + signersStr[i] = addr + accountIdentifiers[i] = &types.AccountIdentifier{ + Address: addr, + } } // get the metadata request information meta := new(ConstructionPreprocessMetadata) @@ -222,24 +193,22 @@ func (c *Client) PreprocessOperationsToOptions(_ context.Context, req *types.Con GasPrice: meta.GasPrice, } - return options.ToMetadata() + metaOptions, err := options.ToMetadata() + if err != nil { + return nil, err + } + return &types.ConstructionPreprocessResponse{ + Options: metaOptions, + RequiredPublicKeys: accountIdentifiers, + }, nil } func (c *Client) AccountIdentifierFromPublicKey(pubKey *types.PublicKey) (*types.AccountIdentifier, error) { - if pubKey.CurveType != "secp256k1" { - return nil, crgerrs.WrapError(crgerrs.ErrUnsupportedCurve, "only secp256k1 supported") - } - - cmp, err := btcec.ParsePubKey(pubKey.Bytes, btcec.S256()) + pk, err := c.converter.ToSDK().PubKey(pubKey) if err != nil { - return nil, crgerrs.WrapError(crgerrs.ErrBadArgument, err.Error()) + return nil, err } - compressedPublicKey := make([]byte, secp256k1.PubKeySize) - copy(compressedPublicKey, cmp.SerializeCompressed()) - - pk := secp256k1.PubKey{Key: compressedPublicKey} - return &types.AccountIdentifier{ Address: sdk.AccAddress(pk.Address()).String(), }, nil diff --git a/server/rosetta/client_online.go b/server/rosetta/client_online.go index 68b862994d3a..86182ff65f1b 100644 --- a/server/rosetta/client_online.go +++ b/server/rosetta/client_online.go @@ -4,9 +4,7 @@ import ( "bytes" "context" "encoding/hex" - "encoding/json" "fmt" - "log" "strconv" "time" @@ -31,6 +29,8 @@ import ( authtx "github.com/cosmos/cosmos-sdk/x/auth/tx" auth "github.com/cosmos/cosmos-sdk/x/auth/types" bank "github.com/cosmos/cosmos-sdk/x/bank/types" + + tmrpc "github.com/tendermint/tendermint/rpc/client" ) // interface assertion @@ -48,10 +48,11 @@ type Client struct { ir codectypes.InterfaceRegistry - clientCtx client.Context + tmRPC tmrpc.Client - txDecoder sdk.TxDecoder - version string + txEncode sdk.TxEncoder + txConfig client.TxConfig + version string converter Converter } @@ -65,12 +66,18 @@ func NewClient(cfg *Config) (*Client, error) { v = "unknown" } + txConfig := authtx.NewTxConfig(cfg.Codec, authtx.DefaultSignModes) + return &Client{ config: cfg, + auth: nil, + bank: nil, ir: cfg.InterfaceRegistry, + tmRPC: nil, + txEncode: txConfig.TxEncoder(), + txConfig: txConfig, version: fmt.Sprintf("%s/%s", info.AppName, v), - txDecoder: authtx.NewTxConfig(cfg.Codec, authtx.DefaultSignModes).TxDecoder(), - converter: NewConverter(cfg.Codec, cfg.InterfaceRegistry, authtx.NewTxConfig(cfg.Codec, authtx.DefaultSignModes)), + converter: NewConverter(cfg.Codec, cfg.InterfaceRegistry, txConfig), }, nil } @@ -114,7 +121,7 @@ func (c *Client) Balances(ctx context.Context, addr string, height *int64) ([]*r return nil, err } - return c.converter.ToRosetta().CoinsToAmounts(balance.Balances, availableCoins), nil + return c.converter.ToRosetta().Amounts(balance.Balances, availableCoins), nil } func (c *Client) BlockByHash(ctx context.Context, hash string) (crgtypes.BlockResponse, error) { @@ -123,7 +130,7 @@ func (c *Client) BlockByHash(ctx context.Context, hash string) (crgtypes.BlockRe return crgtypes.BlockResponse{}, fmt.Errorf("invalid block hash: %s", err) } - block, err := c.clientCtx.Client.BlockByHash(ctx, bHash) + block, err := c.tmRPC.BlockByHash(ctx, bHash) if err != nil { return crgtypes.BlockResponse{}, crgerrs.WrapError(crgerrs.ErrBadGateway, err.Error()) } @@ -132,7 +139,7 @@ func (c *Client) BlockByHash(ctx context.Context, hash string) (crgtypes.BlockRe } func (c *Client) BlockByHeight(ctx context.Context, height *int64) (crgtypes.BlockResponse, error) { - block, err := c.clientCtx.Client.Block(ctx, height) + block, err := c.tmRPC.Block(ctx, height) if err != nil { return crgtypes.BlockResponse{}, crgerrs.WrapError(crgerrs.ErrBadGateway, err.Error()) } @@ -155,8 +162,6 @@ func (c *Client) BlockTransactionsByHeight(ctx context.Context, height *int64) ( if err != nil { return crgtypes.BlockTransactionsResponse{}, err } - bb, _ := json.Marshal(blockTxResp) - log.Printf("block %d: %s", height, bb) return blockTxResp, nil } @@ -170,34 +175,17 @@ func (c *Client) coins(ctx context.Context) (sdk.Coins, error) { } func (c *Client) TxOperationsAndSignersAccountIdentifiers(signed bool, txBytes []byte) (ops []*rosettatypes.Operation, signers []*rosettatypes.AccountIdentifier, err error) { - txConfig := c.getTxConfig() - rawTx, err := c.txDecoder(txBytes) - if err != nil { - return nil, nil, err - } - - txBldr, err := txConfig.WrapTxBuilder(rawTx) - if err != nil { - return nil, nil, err - } - - var accountIdentifierSigners []*rosettatypes.AccountIdentifier - if signed { - addrs := txBldr.GetTx().GetSigners() - for _, addr := range addrs { - signer := &rosettatypes.AccountIdentifier{ - Address: addr.String(), - } - accountIdentifierSigners = append(accountIdentifierSigners, signer) + switch signed { + case false: + rosTx, err := c.converter.ToRosetta().Tx(txBytes, nil) + if err != nil { + return nil, nil, err } + return rosTx.Operations, nil, err + default: + ops, signers, err = c.converter.ToRosetta().OpsAndSigners(txBytes) + return } - - rosTx, err := c.converter.ToRosetta().Tx(txBytes, nil) - if err != nil { - return nil, nil, err - } - - return rosTx.Operations, accountIdentifierSigners, nil } // GetTx returns a transaction given its hash, in rosetta begin block and end block are mocked @@ -209,14 +197,14 @@ func (c *Client) GetTx(ctx context.Context, hash string) (*rosettatypes.Transact } // get tx type and hash - txType, hashBytes := c.converter.FromRosetta().HashToTxType(hashBytes) + txType, hashBytes := c.converter.ToSDK().HashToTxType(hashBytes) // construct rosetta tx switch txType { // handle begin block hash case BeginBlockTx: // get block height by hash - block, err := c.clientCtx.Client.BlockByHash(ctx, hashBytes) + block, err := c.tmRPC.BlockByHash(ctx, hashBytes) if err != nil { return nil, crgerrs.WrapError(crgerrs.ErrUnknown, err.Error()) } @@ -230,16 +218,15 @@ func (c *Client) GetTx(ctx context.Context, hash string) (*rosettatypes.Transact return fullBlock.Transactions[0], nil // handle deliver tx hash case DeliverTxTx: - rawTx, err := c.clientCtx.Client.Tx(ctx, hashBytes, true) + rawTx, err := c.tmRPC.Tx(ctx, hashBytes, true) if err != nil { - log.Printf("tx err: %s : %s", hash, err) return nil, crgerrs.WrapError(crgerrs.ErrUnknown, err.Error()) } return c.converter.ToRosetta().Tx(rawTx.Tx, &rawTx.TxResult) // handle end block hash case EndBlockTx: // get block height by hash - block, err := c.clientCtx.Client.BlockByHash(ctx, hashBytes) + block, err := c.tmRPC.BlockByHash(ctx, hashBytes) if err != nil { return nil, crgerrs.WrapError(crgerrs.ErrUnknown, err.Error()) } @@ -260,7 +247,7 @@ func (c *Client) GetTx(ctx context.Context, hash string) (*rosettatypes.Transact // GetUnconfirmedTx gets an unconfirmed transaction given its hash func (c *Client) GetUnconfirmedTx(ctx context.Context, hash string) (*rosettatypes.Transaction, error) { - res, err := c.clientCtx.Client.UnconfirmedTxs(ctx, nil) + res, err := c.tmRPC.UnconfirmedTxs(ctx, nil) if err != nil { return nil, crgerrs.WrapError(crgerrs.ErrNotFound, "unconfirmed tx not found") } @@ -293,7 +280,7 @@ func (c *Client) GetUnconfirmedTx(ctx context.Context, hash string) (*rosettatyp // Mempool returns the unconfirmed transactions in the mempool func (c *Client) Mempool(ctx context.Context) ([]*rosettatypes.TransactionIdentifier, error) { - txs, err := c.clientCtx.Client.UnconfirmedTxs(ctx, nil) + txs, err := c.tmRPC.UnconfirmedTxs(ctx, nil) if err != nil { return nil, err } @@ -303,7 +290,7 @@ func (c *Client) Mempool(ctx context.Context) ([]*rosettatypes.TransactionIdenti // Peers gets the number of peers func (c *Client) Peers(ctx context.Context) ([]*rosettatypes.Peer, error) { - netInfo, err := c.clientCtx.Client.NetInfo(ctx) + netInfo, err := c.tmRPC.NetInfo(ctx) if err != nil { return nil, crgerrs.WrapError(crgerrs.ErrUnknown, err.Error()) } @@ -311,40 +298,39 @@ func (c *Client) Peers(ctx context.Context) ([]*rosettatypes.Peer, error) { } func (c *Client) Status(ctx context.Context) (*rosettatypes.SyncStatus, error) { - status, err := c.clientCtx.Client.Status(ctx) + status, err := c.tmRPC.Status(ctx) if err != nil { return nil, crgerrs.WrapError(crgerrs.ErrUnknown, err.Error()) } - return c.converter.ToRosetta().StatusToSyncStatus(status), err -} - -func (c *Client) getTxConfig() client.TxConfig { - return c.clientCtx.TxConfig + return c.converter.ToRosetta().SyncStatus(status), err } func (c *Client) PostTx(txBytes []byte) (*rosettatypes.TransactionIdentifier, map[string]interface{}, error) { // sync ensures it will go through checkTx - res, err := c.clientCtx.BroadcastTxSync(txBytes) + res, err := c.tmRPC.BroadcastTxSync(context.Background(), txBytes) if err != nil { return nil, nil, crgerrs.WrapError(crgerrs.ErrUnknown, err.Error()) } // check if tx was broadcast successfully if res.Code != abcitypes.CodeTypeOK { - return nil, nil, crgerrs.WrapError(crgerrs.ErrUnknown, fmt.Sprintf("transaction broadcast failure: (%d) %s ", res.Code, res.RawLog)) + return nil, nil, crgerrs.WrapError( + crgerrs.ErrUnknown, + fmt.Sprintf("transaction broadcast failure: (%d) %s ", res.Code, res.Log), + ) } return &rosettatypes.TransactionIdentifier{ - Hash: res.TxHash, + Hash: fmt.Sprintf("%X", res.Hash), }, map[string]interface{}{ - Log: res.RawLog, + Log: res.Log, }, nil } func (c *Client) Ready() error { ctx, cancel := context.WithTimeout(context.Background(), defaultNodeTimeout) defer cancel() - _, err := c.clientCtx.Client.Health(ctx) + _, err := c.tmRPC.Health(ctx) if err != nil { return err } @@ -386,7 +372,7 @@ func (c *Client) Bootstrap() error { c.auth = authClient c.bank = bankClient - c.clientCtx = clientCtx + c.tmRPC = clientCtx.Client c.ir = c.config.InterfaceRegistry return nil @@ -421,7 +407,7 @@ func (c *Client) ConstructionMetadataFromOptions(ctx context.Context, options ma } } - status, err := c.clientCtx.Client.Status(ctx) + status, err := c.tmRPC.Status(ctx) if err != nil { return nil, err } @@ -439,12 +425,12 @@ func (c *Client) ConstructionMetadataFromOptions(ctx context.Context, options ma func (c *Client) blockTxs(ctx context.Context, height *int64) (crgtypes.BlockTransactionsResponse, error) { // get block info - blockInfo, err := c.clientCtx.Client.Block(ctx, height) + blockInfo, err := c.tmRPC.Block(ctx, height) if err != nil { return crgtypes.BlockTransactionsResponse{}, err } // get block events - blockResults, err := c.clientCtx.Client.BlockResults(ctx, height) + blockResults, err := c.tmRPC.BlockResults(ctx, height) if err != nil { return crgtypes.BlockTransactionsResponse{}, err } @@ -458,7 +444,7 @@ func (c *Client) blockTxs(ctx context.Context, height *int64) (crgtypes.BlockTra TransactionIdentifier: &rosettatypes.TransactionIdentifier{Hash: c.converter.ToRosetta().BeginBlockTxHash(blockInfo.BlockID.Hash)}, Operations: AddOperationIndexes( nil, - c.converter.ToRosetta().EventsToBalanceOps(StatusTxSuccess, blockResults.BeginBlockEvents), + c.converter.ToRosetta().BalanceOps(StatusTxSuccess, blockResults.BeginBlockEvents), ), } @@ -466,7 +452,7 @@ func (c *Client) blockTxs(ctx context.Context, height *int64) (crgtypes.BlockTra TransactionIdentifier: &rosettatypes.TransactionIdentifier{Hash: c.converter.ToRosetta().EndBlockTxHash(blockInfo.BlockID.Hash)}, Operations: AddOperationIndexes( nil, - c.converter.ToRosetta().EventsToBalanceOps(StatusTxSuccess, blockResults.EndBlockEvents), + c.converter.ToRosetta().BalanceOps(StatusTxSuccess, blockResults.EndBlockEvents), ), } diff --git a/server/rosetta/client_v2_test.go b/server/rosetta/client_v2_test.go index 1e0c0c689bea..477ff1a236b2 100644 --- a/server/rosetta/client_v2_test.go +++ b/server/rosetta/client_v2_test.go @@ -2,8 +2,6 @@ package rosetta import ( "context" - "encoding/hex" - "encoding/json" "testing" ) @@ -48,35 +46,3 @@ func TestClientV2(t *testing.T) { } } } - -func TestT(t *testing.T) { - - const addr = "FD41B1F4CA2222B503F448C481873FA91BAE8492495251602A4E60A73C38F944" - cdc, ir := MakeCodec() - c, err := NewClient(&Config{ - Blockchain: "", - Network: "", - TendermintRPC: "tcp://localhost:26657", - GRPCEndpoint: "localhost:9090", - Addr: "", - Retries: 0, - Offline: false, - Codec: cdc, - InterfaceRegistry: ir, - }) - if err != nil { - t.Fatal(err) - } - c.Bootstrap() - hashBy, err := hex.DecodeString(addr) - tx, err := c.clientCtx.Client.Tx(context.Background(), hashBy, true) - t.Logf("%#v", tx.TxResult) - - // ros resp - rosTx, err := c.GetTx(context.TODO(), addr) - if err != nil { - t.Fatal(err) - } - x, err := json.Marshal(rosTx) - t.Logf("%s", x) -} diff --git a/server/rosetta/converter.go b/server/rosetta/converter.go index f18e566f24ed..3ddb0b57e484 100644 --- a/server/rosetta/converter.go +++ b/server/rosetta/converter.go @@ -3,9 +3,14 @@ package rosetta import ( "encoding/json" "fmt" + "reflect" + + "github.com/btcsuite/btcd/btcec" crgtypes "github.com/tendermint/cosmos-rosetta-gateway/types" tmcoretypes "github.com/tendermint/tendermint/rpc/core/types" - "reflect" + + "github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1" + "github.com/cosmos/cosmos-sdk/types/tx/signing" rosettatypes "github.com/coinbase/rosetta-sdk-go/types" "github.com/gogo/protobuf/proto" @@ -16,15 +21,26 @@ import ( sdkclient "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/codec" codectypes "github.com/cosmos/cosmos-sdk/codec/types" + cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" sdk "github.com/cosmos/cosmos-sdk/types" authsigning "github.com/cosmos/cosmos-sdk/x/auth/signing" banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" ) -// converter is a utility that can be used to convert +// Converter is a utility that can be used to convert // back and forth from rosetta to sdk and tendermint types +// IMPORTANT NOTES: +// - IT SHOULD BE USED ONLY TO DEAL WITH THINGS +// IN A STATELESS WAY! IT SHOULD NEVER INTERACT DIRECTLY +// WITH TENDERMINT RPC AND COSMOS GRPC +// +// - IT SHOULD RETURN cosmos rosetta gateway error types! type Converter interface { - FromRosetta() FromRosettaConverter + // ToSDK exposes the methods that convert + // rosetta types to cosmos sdk and tendermint types + ToSDK() ToSDKConverter + // ToRosetta exposes the methods that convert + // sdk and tendermint types to rosetta types ToRosetta() ToRosettaConverter } @@ -38,38 +54,51 @@ type ToRosettaConverter interface { BeginBlockTxHash(blockHash []byte) string // EndBlockTxHash converts the given endblock hash to rosetta transaction hash EndBlockTxHash(blockHash []byte) string - // CoinsToAmounts converts sdk.Coins to amounts - CoinsToAmounts(ownedCoins []sdk.Coin, availableCoins sdk.Coins) []*rosettatypes.Amount - // MsgToOps converts an sdk.Msg to a rosetta operation - MsgToOps(status string, msg sdk.Msg) ([]*rosettatypes.Operation, error) - // MsgToMeta converts an sdk.Msg to rosetta metadata - MsgToMeta(msg sdk.Msg) (meta map[string]interface{}, err error) + // Amounts converts sdk.Coins to rosetta.Amounts + Amounts(ownedCoins []sdk.Coin, availableCoins sdk.Coins) []*rosettatypes.Amount + // Ops converts an sdk.Msg to rosetta operations + Ops(status string, msg sdk.Msg) ([]*rosettatypes.Operation, error) + // OpsAndSigners takes raw transaction bytes and returns rosetta operations and the expected signers + OpsAndSigners(txBytes []byte) (ops []*rosettatypes.Operation, signers []*rosettatypes.AccountIdentifier, err error) + // Meta converts an sdk.Msg to rosetta metadata + Meta(msg sdk.Msg) (meta map[string]interface{}, err error) // Tx converts a tendermint transaction and tx result if provided to a rosetta tx Tx(rawTx tmtypes.Tx, txResult *abci.ResponseDeliverTx) (*rosettatypes.Transaction, error) // TxIdentifiers converts a tendermint tx to transaction identifiers TxIdentifiers(txs []tmtypes.Tx) []*rosettatypes.TransactionIdentifier - // EventsToBalanceOps converts events to balance operations - EventsToBalanceOps(status string, events []abci.Event) []*rosettatypes.Operation - // StatusToSyncStatus converts a tendermint status to sync status - StatusToSyncStatus(status *tmcoretypes.ResultStatus) *rosettatypes.SyncStatus + // BalanceOps converts events to balance operations + BalanceOps(status string, events []abci.Event) []*rosettatypes.Operation + // SyncStatus converts a tendermint status to sync status + SyncStatus(status *tmcoretypes.ResultStatus) *rosettatypes.SyncStatus // Peers converts tendermint peers to rosetta Peers(peers []tmcoretypes.Peer) []*rosettatypes.Peer } -// FromRosettaConverter is an interface that exposes +// ToSDKConverter is an interface that exposes // all the functions used to convert rosetta types // to tendermint and sdk types -type FromRosettaConverter interface { - OpsToUnsignedTx(ops []*rosettatypes.Operation) (tx authsigning.Tx, err error) - MetaToMsg(meta map[string]interface{}, msg sdk.Msg) (err error) +type ToSDKConverter interface { + // UnsignedTx converts rosetta operations to an unsigned cosmos sdk transactions + UnsignedTx(ops []*rosettatypes.Operation) (tx authsigning.Tx, err error) + // SignedTx adds the provided signatures after decoding the unsigned transaction raw bytes + // and returns the signed tx bytes + SignedTx(txBytes []byte, signatures []*rosettatypes.Signature) (signedTxBytes []byte, err error) + // Msg converts metadata to an sdk message + Msg(meta map[string]interface{}, msg sdk.Msg) (err error) + // HashToTxType returns the transaction type (end block, begin block or deliver tx) + // and the real hash to query in order to get information HashToTxType(hashBytes []byte) (txType TransactionType, realHash []byte) + // PubKey attempts to convert a rosetta public key to cosmos sdk one + PubKey(pk *rosettatypes.PublicKey) (cryptotypes.PubKey, error) } type converter struct { - newTxBuilder func() sdkclient.TxBuilder - txDecode sdk.TxDecoder - ir codectypes.InterfaceRegistry - cdc *codec.ProtoCodec + newTxBuilder func() sdkclient.TxBuilder + txBuilderFromTx func(tx sdk.Tx) (sdkclient.TxBuilder, error) + txDecode sdk.TxDecoder + txEncode sdk.TxEncoder + ir codectypes.InterfaceRegistry + cdc *codec.ProtoCodec } func NewConverter(cdc *codec.ProtoCodec, ir codectypes.InterfaceRegistry, cfg sdkclient.TxConfig) Converter { @@ -77,13 +106,17 @@ func NewConverter(cdc *codec.ProtoCodec, ir codectypes.InterfaceRegistry, cfg sd newTxBuilder: func() sdkclient.TxBuilder { return cfg.NewTxBuilder() }, + txBuilderFromTx: func(tx sdk.Tx) (sdkclient.TxBuilder, error) { + return cfg.WrapTxBuilder(tx) + }, + txDecode: cfg.TxDecoder(), + txEncode: cfg.TxEncoder(), ir: ir, cdc: cdc, - txDecode: cfg.TxDecoder(), } } -func (c converter) FromRosetta() FromRosettaConverter { +func (c converter) ToSDK() ToSDKConverter { return c } @@ -92,7 +125,7 @@ func (c converter) ToRosetta() ToRosettaConverter { } // OpsToUnsignedTx returns all the sdk.Msgs given the operations -func (c converter) OpsToUnsignedTx(ops []*rosettatypes.Operation) (tx authsigning.Tx, err error) { +func (c converter) UnsignedTx(ops []*rosettatypes.Operation) (tx authsigning.Tx, err error) { builder := c.newTxBuilder() var msgs []sdk.Msg @@ -110,7 +143,7 @@ func (c converter) OpsToUnsignedTx(ops []*rosettatypes.Operation) (tx authsignin return nil, crgerrs.WrapError(crgerrs.ErrBadArgument, "operation is not a valid supported sdk.Msg: "+op.Type) } - err = c.MetaToMsg(op.Metadata, msg) + err = c.Msg(op.Metadata, msg) if err != nil { return nil, crgerrs.WrapError(crgerrs.ErrCodec, err.Error()) } @@ -168,7 +201,7 @@ func (c converter) OpsToUnsignedTx(ops []*rosettatypes.Operation) (tx authsignin } // MetaToMsg unmarshals the rosetta metadata to the given sdk.Msg -func (c converter) MetaToMsg(meta map[string]interface{}, msg sdk.Msg) error { +func (c converter) Msg(meta map[string]interface{}, msg sdk.Msg) error { metaBytes, err := json.Marshal(meta) if err != nil { return err @@ -176,7 +209,7 @@ func (c converter) MetaToMsg(meta map[string]interface{}, msg sdk.Msg) error { return c.cdc.UnmarshalJSON(metaBytes, msg) } -func (c converter) MsgToMeta(msg sdk.Msg) (meta map[string]interface{}, err error) { +func (c converter) Meta(msg sdk.Msg) (meta map[string]interface{}, err error) { b, err := c.cdc.MarshalJSON(msg) if err != nil { return nil, crgerrs.WrapError(crgerrs.ErrCodec, err.Error()) @@ -193,7 +226,7 @@ func (c converter) MsgToMeta(msg sdk.Msg) (meta map[string]interface{}, err erro // MsgToOps will create an operation for each msg signer // with the message proto name as type, and the raw fields // as metadata -func (c converter) MsgToOps(status string, msg sdk.Msg) ([]*rosettatypes.Operation, error) { +func (c converter) Ops(status string, msg sdk.Msg) ([]*rosettatypes.Operation, error) { opName := proto.MessageName(msg) // in case proto does not recognize the message name // then we should try to cast it to service msg, to @@ -220,7 +253,7 @@ func (c converter) MsgToOps(status string, msg sdk.Msg) ([]*rosettatypes.Operati } } - meta, err := c.MsgToMeta(msg) + meta, err := c.Meta(msg) if err != nil { return nil, err } @@ -267,7 +300,7 @@ func (c converter) Tx(rawTx tmtypes.Tx, txResult *abci.ResponseDeliverTx) (*rose msgs := tx.GetMsgs() var rawTxOps []*rosettatypes.Operation for _, msg := range msgs { - ops, err := c.MsgToOps(status, msg) + ops, err := c.Ops(status, msg) if err != nil { return nil, err } @@ -278,7 +311,7 @@ func (c converter) Tx(rawTx tmtypes.Tx, txResult *abci.ResponseDeliverTx) (*rose var balanceOps []*rosettatypes.Operation // tx result might be nil, in case we're querying an unconfirmed tx from the mempool if txResult != nil { - balanceOps = c.EventsToBalanceOps(status, txResult.Events) + balanceOps = c.BalanceOps(status, txResult.Events) } // now normalize indexes @@ -290,7 +323,7 @@ func (c converter) Tx(rawTx tmtypes.Tx, txResult *abci.ResponseDeliverTx) (*rose }, nil } -func (c converter) EventsToBalanceOps(status string, events []abci.Event) []*rosettatypes.Operation { +func (c converter) BalanceOps(status string, events []abci.Event) []*rosettatypes.Operation { var ops []*rosettatypes.Operation for _, e := range events { @@ -389,7 +422,7 @@ func sdkEventToBalanceOperations(status string, event abci.Event) (operations [] } // sdkCoinsToRosettaAmounts converts []sdk.Coin to rosetta amounts -func (c converter) CoinsToAmounts(ownedCoins []sdk.Coin, availableCoins sdk.Coins) []*rosettatypes.Amount { +func (c converter) Amounts(ownedCoins []sdk.Coin, availableCoins sdk.Coins) []*rosettatypes.Amount { amounts := make([]*rosettatypes.Amount, len(availableCoins)) ownedCoinsMap := make(map[string]sdk.Int, len(availableCoins)) @@ -487,7 +520,7 @@ func (c converter) HashToTxType(hashBytes []byte) (txType TransactionType, realH } // StatusToSyncStatus converts a tendermint status to rosetta sync status -func (c converter) StatusToSyncStatus(status *tmcoretypes.ResultStatus) *rosettatypes.SyncStatus { +func (c converter) SyncStatus(status *tmcoretypes.ResultStatus) *rosettatypes.SyncStatus { // determine sync status var stage = StatusPeerSynced if status.SyncInfo.CatchingUp { @@ -553,3 +586,92 @@ func (c converter) Peers(peers []tmcoretypes.Peer) []*rosettatypes.Peer { return converted } + +// OpsAndSigners takes transactions bytes and returns the operation, is signed is true it will return +// the account identifiers which have signed the transaction +func (c converter) OpsAndSigners(txBytes []byte) (ops []*rosettatypes.Operation, signers []*rosettatypes.AccountIdentifier, err error) { + + rosTx, err := c.ToRosetta().Tx(txBytes, nil) + if err != nil { + return nil, nil, err + } + ops = rosTx.Operations + + // get the signers + sdkTx, err := c.txDecode(txBytes) + if err != nil { + return nil, nil, crgerrs.WrapError(crgerrs.ErrCodec, err.Error()) + } + + txBuilder, err := c.txBuilderFromTx(sdkTx) + if err != nil { + return nil, nil, crgerrs.WrapError(crgerrs.ErrCodec, err.Error()) + } + + for _, signer := range txBuilder.GetTx().GetSigners() { + signers = append(signers, &rosettatypes.AccountIdentifier{ + Address: signer.String(), + }) + } + + return +} + +func (c converter) SignedTx(txBytes []byte, signatures []*rosettatypes.Signature) (signedTxBytes []byte, err error) { + rawTx, err := c.txDecode(txBytes) + if err != nil { + return nil, err + } + + txBuilder, err := c.txBuilderFromTx(rawTx) + if err != nil { + return nil, err + } + + notSignedSigs, err := txBuilder.GetTx().GetSignaturesV2() // + if err != nil { + return nil, crgerrs.WrapError(crgerrs.ErrCodec, err.Error()) + } + + signedSigs := make([]signing.SignatureV2, len(notSignedSigs)) + for i, signature := range signatures { + // TODO(fdymylja): here we should check that the public key matches... + signedSigs[i] = signing.SignatureV2{ + PubKey: notSignedSigs[i].PubKey, + Data: &signing.SingleSignatureData{ + SignMode: signing.SignMode_SIGN_MODE_LEGACY_AMINO_JSON, + Signature: signature.Bytes, + }, + Sequence: notSignedSigs[i].Sequence, + } + } + + if err = txBuilder.SetSignatures(signedSigs...); err != nil { + return nil, err + } + + txBytes, err = c.txEncode(txBuilder.GetTx()) + if err != nil { + return nil, err + } + + return txBytes, nil +} + +func (c converter) PubKey(pubKey *rosettatypes.PublicKey) (cryptotypes.PubKey, error) { + if pubKey.CurveType != "secp256k1" { + return nil, crgerrs.WrapError(crgerrs.ErrUnsupportedCurve, "only secp256k1 supported") + } + + cmp, err := btcec.ParsePubKey(pubKey.Bytes, btcec.S256()) + if err != nil { + return nil, crgerrs.WrapError(crgerrs.ErrBadArgument, err.Error()) + } + + compressedPublicKey := make([]byte, secp256k1.PubKeySize) + copy(compressedPublicKey, cmp.SerializeCompressed()) + + pk := &secp256k1.PubKey{Key: compressedPublicKey} + + return pk, nil +} diff --git a/server/rosetta/converter_test.go b/server/rosetta/converter_test.go index e1b599b962cc..fff4f476f717 100644 --- a/server/rosetta/converter_test.go +++ b/server/rosetta/converter_test.go @@ -1,6 +1,7 @@ package rosetta import ( + "encoding/hex" "testing" rosettatypes "github.com/coinbase/rosetta-sdk-go/types" @@ -40,23 +41,23 @@ func (s *ConverterTestSuite) TestFromRosettaOpsToTxSuccess() { Amount: sdk.NewCoins(sdk.NewInt64Coin("utxo", 10)), } - ops, err := s.c.ToRosetta().MsgToOps("", msg1) + ops, err := s.c.ToRosetta().Ops("", msg1) s.Require().NoError(err) - ops2, err := s.c.ToRosetta().MsgToOps("", msg2) + ops2, err := s.c.ToRosetta().Ops("", msg2) s.Require().NoError(err) ops = append(ops, ops2...) - tx, err := s.c.FromRosetta().OpsToUnsignedTx(ops) + tx, err := s.c.ToSDK().UnsignedTx(ops) s.Require().NoError(err) - msgs := tx.GetMsgs() + getMsgs := tx.GetMsgs() - s.Require().Equal(2, len(msgs)) + s.Require().Equal(2, len(getMsgs)) - s.Require().Equal(msgs[0], msg1) - s.Require().Equal(msgs[1], msg2) + s.Require().Equal(getMsgs[0], msg1) + s.Require().Equal(getMsgs[1], msg2) } @@ -66,7 +67,7 @@ func (s *ConverterTestSuite) TestFromRosettaOpsToTxErrors() { Type: "non-existent", } - _, err := s.c.FromRosetta().OpsToUnsignedTx([]*rosettatypes.Operation{op}) + _, err := s.c.ToSDK().UnsignedTx([]*rosettatypes.Operation{op}) s.Require().ErrorIs(err, crgerrs.ErrBadArgument) }) @@ -76,7 +77,7 @@ func (s *ConverterTestSuite) TestFromRosettaOpsToTxErrors() { Type: "cosmos.crypto.ed25519.PubKey", } - _, err := s.c.FromRosetta().OpsToUnsignedTx([]*rosettatypes.Operation{op}) + _, err := s.c.ToSDK().UnsignedTx([]*rosettatypes.Operation{op}) s.Require().ErrorIs(err, crgerrs.ErrBadArgument) @@ -93,12 +94,12 @@ func (s *ConverterTestSuite) TestMsgToMetaMetaToMsg() { msg.Route() - meta, err := s.c.ToRosetta().MsgToMeta(msg) + meta, err := s.c.ToRosetta().Meta(msg) s.Require().NoError(err) copyMsg := new(bank.MsgSend) - err = s.c.FromRosetta().MetaToMsg(meta, copyMsg) + err = s.c.ToSDK().Msg(meta, copyMsg) s.Require().NoError(err) s.Require().Equal(msg, copyMsg) @@ -107,3 +108,17 @@ func (s *ConverterTestSuite) TestMsgToMetaMetaToMsg() { func TestConverterTestSuite(t *testing.T) { suite.Run(t, new(ConverterTestSuite)) } + +func (s *ConverterTestSuite) TestX() { + const txRaw = "0a8e010a8b010a1c2f636f736d6f732e62616e6b2e763162657461312e4d736753656e64126b0a2d636f736d6f7331656e377a6574686b6c6c79307761386a7778777878727638396565386a383668656374747337122d636f736d6f73317377383670393076393076753875706d363478327173373068756663796330746d34766e37361a0b0a057374616b651202313812600a4c0a460a1f2f636f736d6f732e63727970746f2e736563703235366b312e5075624b657912230a21030c65f93f08cc27ee461ce1cd0a11647f0cfe852d044874d4d0905240a9787ec412020a0012100a0a0a057374616b651201311090a10f1a00" + + raw, _ := hex.DecodeString(txRaw) + cdc, _ := MakeCodec() + txConfig := authtx.NewTxConfig(cdc, authtx.DefaultSignModes) + + tx, err := txConfig.TxDecoder()(raw) + s.Require().NoError(err) + txB, err := txConfig.TxJSONEncoder()(tx) + s.Require().NoError(err) + s.T().Logf("%s", txB) +} From 232d83feffdac7cb0ae0c64f98200206dd8382dd Mon Sep 17 00:00:00 2001 From: Frojdi Dymylja Date: Thu, 4 Mar 2021 18:31:03 +0100 Subject: [PATCH 07/33] chore: cleanup api --- server/rosetta/client_offline.go | 75 ++------------------- server/rosetta/client_online.go | 2 - server/rosetta/converter.go | 109 ++++++++++++++++++++++++++++++- 3 files changed, 112 insertions(+), 74 deletions(-) diff --git a/server/rosetta/client_offline.go b/server/rosetta/client_offline.go index 1fc9a46b8d38..8c3d094c2fa8 100644 --- a/server/rosetta/client_offline.go +++ b/server/rosetta/client_offline.go @@ -3,21 +3,14 @@ package rosetta import ( "context" "encoding/hex" - "encoding/json" - "log" "strings" - "github.com/tendermint/tendermint/crypto" - - authsigning "github.com/cosmos/cosmos-sdk/x/auth/signing" - banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" "github.com/coinbase/rosetta-sdk-go/types" crgerrs "github.com/tendermint/cosmos-rosetta-gateway/errors" sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/cosmos/cosmos-sdk/types/tx/signing" ) func (c *Client) OperationStatuses() []*types.OperationStatus { @@ -63,8 +56,6 @@ func (c *Client) SignedTx(_ context.Context, txBytes []byte, signatures []*types } func (c *Client) ConstructionPayload(_ context.Context, request *types.ConstructionPayloadsRequest) (resp *types.ConstructionPayloadsResponse, err error) { - b, _ := json.Marshal(request) - log.Printf("raw req: %s", b) // check if there is at least one operation if len(request.Operations) < 1 { return nil, crgerrs.WrapError(crgerrs.ErrInvalidOperation, "expected at least one operation") @@ -77,72 +68,16 @@ func (c *Client) ConstructionPayload(_ context.Context, request *types.Construct metadata := new(ConstructionMetadata) if err = metadata.FromMetadata(request.Metadata); err != nil { - return nil, crgerrs.WrapError(crgerrs.ErrBadArgument, err.Error()) - } - feeAmt, err := sdk.ParseCoinsNormalized(metadata.GasPrice) - if err != nil { - return nil, crgerrs.WrapError(crgerrs.ErrBadArgument, err.Error()) - } - - // - builder := c.txConfig.NewTxBuilder() - _ = builder.SetMsgs(tx.GetMsgs()...) - builder.SetFeeAmount(feeAmt) - builder.SetGasLimit(metadata.GasLimit) - builder.SetMemo(metadata.Memo) - - tx = builder.GetTx() - - accIdentifiers := tx.GetSigners() - - payloads := make([]*types.SigningPayload, len(accIdentifiers)) - signersData := make([]signing.SignatureV2, len(accIdentifiers)) - for i, accID := range accIdentifiers { - // we expect pubkeys to be ordered... TODO(fdymylja): maybe make ordering not matter? - signerData := authsigning.SignerData{ - ChainID: metadata.ChainID, - AccountNumber: metadata.SignersData[i].AccountNumber, - Sequence: metadata.SignersData[i].Sequence, - } - // Sign_mode_legacy_amino is being used as default here, as sign_mode_direct - // needs the signer infos to be set before hand but rosetta doesn't have a way - // to do this yet. To be revisited in future versions of sdk and rosetta - signBytes, err := c.txConfig.SignModeHandler().GetSignBytes(signing.SignMode_SIGN_MODE_LEGACY_AMINO_JSON, signerData, tx) - if err != nil { - return nil, crgerrs.WrapError(crgerrs.ErrBadArgument, "signing error: "+err.Error()) - } - - payloads[i] = &types.SigningPayload{ - AccountIdentifier: &types.AccountIdentifier{Address: accID.String()}, - Bytes: crypto.Sha256(signBytes), - SignatureType: types.Ecdsa, - } - - pk, err := c.converter.ToSDK().PubKey(request.PublicKeys[i]) - if err != nil { - return nil, err - } - - signersData[i] = signing.SignatureV2{ - PubKey: pk, - Data: &signing.SingleSignatureData{}, - Sequence: metadata.SignersData[i].Sequence, - } + return nil, err } - // we set the signature data so we carry information regarding public key - // then afterwards we just need to set the signed bytes - err = builder.SetSignatures(signersData...) - if err != nil { - return nil, crgerrs.WrapError(crgerrs.ErrCodec, err.Error()) - } - // encode tx - encodedTx, err := c.txEncode(builder.GetTx()) + txBytes, payloads, err := c.converter.ToRosetta().SigningComponents(tx, metadata, request.PublicKeys) if err != nil { - return nil, crgerrs.WrapError(crgerrs.ErrCodec, err.Error()) + return nil, err } + return &types.ConstructionPayloadsResponse{ - UnsignedTransaction: hex.EncodeToString(encodedTx), + UnsignedTransaction: hex.EncodeToString(txBytes), Payloads: payloads, }, nil } diff --git a/server/rosetta/client_online.go b/server/rosetta/client_online.go index 86182ff65f1b..94b3f9d99e14 100644 --- a/server/rosetta/client_online.go +++ b/server/rosetta/client_online.go @@ -50,7 +50,6 @@ type Client struct { tmRPC tmrpc.Client - txEncode sdk.TxEncoder txConfig client.TxConfig version string @@ -74,7 +73,6 @@ func NewClient(cfg *Config) (*Client, error) { bank: nil, ir: cfg.InterfaceRegistry, tmRPC: nil, - txEncode: txConfig.TxEncoder(), txConfig: txConfig, version: fmt.Sprintf("%s/%s", info.AppName, v), converter: NewConverter(cfg.Codec, cfg.InterfaceRegistry, txConfig), diff --git a/server/rosetta/converter.go b/server/rosetta/converter.go index 3ddb0b57e484..21ae924c0a6e 100644 --- a/server/rosetta/converter.go +++ b/server/rosetta/converter.go @@ -1,10 +1,13 @@ package rosetta import ( + "bytes" "encoding/json" "fmt" "reflect" + "github.com/tendermint/tendermint/crypto" + "github.com/btcsuite/btcd/btcec" crgtypes "github.com/tendermint/cosmos-rosetta-gateway/types" tmcoretypes "github.com/tendermint/tendermint/rpc/core/types" @@ -62,6 +65,8 @@ type ToRosettaConverter interface { OpsAndSigners(txBytes []byte) (ops []*rosettatypes.Operation, signers []*rosettatypes.AccountIdentifier, err error) // Meta converts an sdk.Msg to rosetta metadata Meta(msg sdk.Msg) (meta map[string]interface{}, err error) + // SigningComponents returns rosetta's components required to build a signable transaction + SigningComponents(tx authsigning.Tx, metadata *ConstructionMetadata, rosPubKeys []*rosettatypes.PublicKey) (txBytes []byte, payloadsToSign []*rosettatypes.SigningPayload, err error) // Tx converts a tendermint transaction and tx result if provided to a rosetta tx Tx(rawTx tmtypes.Tx, txResult *abci.ResponseDeliverTx) (*rosettatypes.Transaction, error) // TxIdentifiers converts a tendermint tx to transaction identifiers @@ -97,6 +102,7 @@ type converter struct { txBuilderFromTx func(tx sdk.Tx) (sdkclient.TxBuilder, error) txDecode sdk.TxDecoder txEncode sdk.TxEncoder + bytesToSign func(tx authsigning.Tx, signerData authsigning.SignerData) (b []byte, err error) ir codectypes.InterfaceRegistry cdc *codec.ProtoCodec } @@ -111,8 +117,16 @@ func NewConverter(cdc *codec.ProtoCodec, ir codectypes.InterfaceRegistry, cfg sd }, txDecode: cfg.TxDecoder(), txEncode: cfg.TxEncoder(), - ir: ir, - cdc: cdc, + bytesToSign: func(tx authsigning.Tx, signerData authsigning.SignerData) (b []byte, err error) { + bytesToSign, err := cfg.SignModeHandler().GetSignBytes(signing.SignMode_SIGN_MODE_LEGACY_AMINO_JSON, signerData, tx) + if err != nil { + return nil, err + } + + return crypto.Sha256(bytesToSign), nil + }, + ir: ir, + cdc: cdc, } } @@ -675,3 +689,94 @@ func (c converter) PubKey(pubKey *rosettatypes.PublicKey) (cryptotypes.PubKey, e return pk, nil } + +// SigningComponents takes a sdk tx and construction metadata and returns signable components +func (c converter) SigningComponents(tx authsigning.Tx, metadata *ConstructionMetadata, rosPubKeys []*rosettatypes.PublicKey) (txBytes []byte, payloadsToSign []*rosettatypes.SigningPayload, err error) { + + // verify metadata correctness + feeAmount, err := sdk.ParseCoinsNormalized(metadata.GasPrice) + if err != nil { + return nil, nil, crgerrs.WrapError(crgerrs.ErrBadArgument, err.Error()) + } + + signers := tx.GetSigners() + // assert the signers data provided in options are the same as the expected signing accounts + // and that the number of rosetta provided public keys equals the one of the signers + if len(metadata.SignersData) != len(signers) || len(signers) != len(rosPubKeys) { + return nil, nil, crgerrs.WrapError(crgerrs.ErrBadArgument, "signers data and account identifiers mismatch") + } + + // add transaction metadata + builder, err := c.txBuilderFromTx(tx) + if err != nil { + return nil, nil, crgerrs.WrapError(crgerrs.ErrCodec, err.Error()) + } + builder.SetFeeAmount(feeAmount) + builder.SetGasLimit(metadata.GasLimit) + builder.SetMemo(metadata.Memo) + + // build signatures + partialSignatures := make([]signing.SignatureV2, len(signers)) + payloadsToSign = make([]*rosettatypes.SigningPayload, len(signers)) + + // pub key ordering matters, in a future release this check might be relaxed + for i, signer := range signers { + // assert that the provided public keys are correctly ordered + // by checking if the signer at index i matches the pubkey at index + pubKey, err := c.ToSDK().PubKey(rosPubKeys[0]) + if err != nil { + return nil, nil, err + } + if !bytes.Equal(pubKey.Address().Bytes(), signer.Bytes()) { + return nil, nil, crgerrs.WrapError( + crgerrs.ErrBadArgument, + fmt.Sprintf("public key at index %d does not match the expected transaction signer: %X <-> %X", i, rosPubKeys[i].Bytes, signer.Bytes()), + ) + } + + // set the signer data + signerData := authsigning.SignerData{ + ChainID: metadata.ChainID, + AccountNumber: metadata.SignersData[i].AccountNumber, + Sequence: metadata.SignersData[i].Sequence, + } + + // get signature bytes + signBytes, err := c.bytesToSign(tx, signerData) + if err != nil { + return nil, nil, crgerrs.WrapError(crgerrs.ErrUnknown, fmt.Sprintf("unable to sign tx: %s", err.Error())) + } + + // set payload + payloadsToSign[i] = &rosettatypes.SigningPayload{ + AccountIdentifier: &rosettatypes.AccountIdentifier{Address: signer.String()}, + Bytes: signBytes, + SignatureType: rosettatypes.Ecdsa, + } + + // set partial signature + partialSignatures[i] = signing.SignatureV2{ + PubKey: pubKey, + Data: &signing.SingleSignatureData{}, // needs to be set to empty otherwise the codec will cry + Sequence: metadata.SignersData[i].Sequence, + } + + } + + // now we set the partial signatures in the tx + // because we will need to decode the sequence + // information of each account in a stateless way + err = builder.SetSignatures(partialSignatures...) + if err != nil { + return nil, nil, crgerrs.WrapError(crgerrs.ErrCodec, err.Error()) + } + + // finally encode the tx + // encode tx + txBytes, err = c.txEncode(builder.GetTx()) + if err != nil { + return nil, nil, crgerrs.WrapError(crgerrs.ErrCodec, err.Error()) + } + + return +} From b43553ece635bccce7578a8ba432a5ed7b4e103d Mon Sep 17 00:00:00 2001 From: Frojdi Dymylja Date: Thu, 4 Mar 2021 18:54:22 +0100 Subject: [PATCH 08/33] chore: cleanup api --- server/rosetta/client_offline.go | 22 +-------- server/rosetta/client_online.go | 81 ++++++++++++++------------------ server/rosetta/converter.go | 18 +++++++ 3 files changed, 54 insertions(+), 67 deletions(-) diff --git a/server/rosetta/client_offline.go b/server/rosetta/client_offline.go index 8c3d094c2fa8..fb193d0aa584 100644 --- a/server/rosetta/client_offline.go +++ b/server/rosetta/client_offline.go @@ -3,9 +3,6 @@ package rosetta import ( "context" "encoding/hex" - "strings" - - banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" "github.com/coinbase/rosetta-sdk-go/types" crgerrs "github.com/tendermint/cosmos-rosetta-gateway/errors" @@ -31,24 +28,7 @@ func (c *Client) Version() string { } func (c *Client) SupportedOperations() []string { - var supportedOperations []string - for _, ii := range c.ir.ListImplementations("cosmos.base.v1beta1.Msg") { - resolvedMsg, err := c.ir.Resolve(ii) - if err != nil { - continue - } - - if _, ok := resolvedMsg.(sdk.Msg); ok { - supportedOperations = append(supportedOperations, strings.TrimLeft(ii, "/")) - } - } - - supportedOperations = append( - supportedOperations, - banktypes.EventTypeCoinSpent, banktypes.EventTypeCoinReceived, - ) - - return supportedOperations + return c.supportedOperations } func (c *Client) SignedTx(_ context.Context, txBytes []byte, signatures []*types.Signature) (signedTxBytes []byte, err error) { diff --git a/server/rosetta/client_online.go b/server/rosetta/client_online.go index 94b3f9d99e14..f8135b9c9ff1 100644 --- a/server/rosetta/client_online.go +++ b/server/rosetta/client_online.go @@ -6,6 +6,7 @@ import ( "encoding/hex" "fmt" "strconv" + "strings" "time" "github.com/cosmos/cosmos-sdk/version" @@ -21,9 +22,6 @@ import ( crgerrs "github.com/tendermint/cosmos-rosetta-gateway/errors" crgtypes "github.com/tendermint/cosmos-rosetta-gateway/types" - "github.com/cosmos/cosmos-sdk/client" - "github.com/cosmos/cosmos-sdk/client/flags" - codectypes "github.com/cosmos/cosmos-sdk/codec/types" sdk "github.com/cosmos/cosmos-sdk/types" grpctypes "github.com/cosmos/cosmos-sdk/types/grpc" authtx "github.com/cosmos/cosmos-sdk/x/auth/tx" @@ -41,17 +39,15 @@ const defaultNodeTimeout = 15 * time.Second // Client implements a single network client to interact with cosmos based chains type Client struct { - config *Config - - auth auth.QueryClient - bank bank.QueryClient + supportedOperations []string - ir codectypes.InterfaceRegistry + config *Config + auth auth.QueryClient + bank bank.QueryClient tmRPC tmrpc.Client - txConfig client.TxConfig - version string + version string converter Converter } @@ -67,19 +63,35 @@ func NewClient(cfg *Config) (*Client, error) { txConfig := authtx.NewTxConfig(cfg.Codec, authtx.DefaultSignModes) + var supportedOperations []string + for _, ii := range cfg.InterfaceRegistry.ListImplementations("cosmos.base.v1beta1.Msg") { + resolvedMsg, err := cfg.InterfaceRegistry.Resolve(ii) + if err != nil { + continue + } + + if _, ok := resolvedMsg.(sdk.Msg); ok { + supportedOperations = append(supportedOperations, strings.TrimLeft(ii, "/")) + } + } + + supportedOperations = append( + supportedOperations, + bank.EventTypeCoinSpent, bank.EventTypeCoinReceived, + ) + return &Client{ - config: cfg, - auth: nil, - bank: nil, - ir: cfg.InterfaceRegistry, - tmRPC: nil, - txConfig: txConfig, - version: fmt.Sprintf("%s/%s", info.AppName, v), - converter: NewConverter(cfg.Codec, cfg.InterfaceRegistry, txConfig), + supportedOperations: supportedOperations, + config: cfg, + auth: nil, + bank: nil, + tmRPC: nil, + version: fmt.Sprintf("%s/%s", info.AppName, v), + converter: NewConverter(cfg.Codec, cfg.InterfaceRegistry, txConfig), }, nil } -func (c *Client) accountInfo(ctx context.Context, addr string, height *int64) (auth.AccountI, error) { +func (c *Client) accountInfo(ctx context.Context, addr string, height *int64) (*SignerData, error) { if height != nil { strHeight := strconv.FormatInt(*height, 10) ctx = metadata.AppendToOutgoingContext(ctx, grpctypes.GRPCBlockHeightHeader, strHeight) @@ -92,13 +104,9 @@ func (c *Client) accountInfo(ctx context.Context, addr string, height *int64) (a return nil, crgerrs.FromGRPCToRosettaError(err) } - var account auth.AccountI - err = c.ir.UnpackAny(accountInfo.Account, &account) - if err != nil { - return nil, crgerrs.WrapError(crgerrs.ErrCodec, err.Error()) - } + signerData, err := c.converter.ToRosetta().SignerData(accountInfo.Account) - return account, nil + return signerData, nil } func (c *Client) Balances(ctx context.Context, addr string, height *int64) ([]*rosettatypes.Amount, error) { @@ -353,25 +361,9 @@ func (c *Client) Bootstrap() error { authClient := auth.NewQueryClient(grpcConn) bankClient := bank.NewQueryClient(grpcConn) - // NodeURI and Client are set from here otherwise - // WitNodeURI will require to create a new client - // it's done here because WithNodeURI panics if - // connection to tendermint node fails - clientCtx := client.Context{ - Client: tmRPC, - NodeURI: c.config.TendermintRPC, - } - clientCtx = clientCtx. - WithJSONMarshaler(c.config.Codec). - WithInterfaceRegistry(c.config.InterfaceRegistry). - WithTxConfig(authtx.NewTxConfig(c.config.Codec, authtx.DefaultSignModes)). - WithAccountRetriever(auth.AccountRetriever{}). - WithBroadcastMode(flags.BroadcastBlock) - c.auth = authClient c.bank = bankClient - c.tmRPC = clientCtx.Client - c.ir = c.config.InterfaceRegistry + c.tmRPC = tmRPC return nil } @@ -399,10 +391,7 @@ func (c *Client) ConstructionMetadataFromOptions(ctx context.Context, options ma return nil, err } - signersData[i] = &SignerData{ - AccountNumber: accountInfo.GetAccountNumber(), - Sequence: accountInfo.GetSequence(), - } + signersData[i] = accountInfo } status, err := c.tmRPC.Status(ctx) diff --git a/server/rosetta/converter.go b/server/rosetta/converter.go index 21ae924c0a6e..ef49dac6fe82 100644 --- a/server/rosetta/converter.go +++ b/server/rosetta/converter.go @@ -6,6 +6,8 @@ import ( "fmt" "reflect" + auth "github.com/cosmos/cosmos-sdk/x/auth/types" + "github.com/tendermint/tendermint/crypto" "github.com/btcsuite/btcd/btcec" @@ -65,6 +67,8 @@ type ToRosettaConverter interface { OpsAndSigners(txBytes []byte) (ops []*rosettatypes.Operation, signers []*rosettatypes.AccountIdentifier, err error) // Meta converts an sdk.Msg to rosetta metadata Meta(msg sdk.Msg) (meta map[string]interface{}, err error) + // SignerData returns account signing data from a queried any account + SignerData(anyAccount *codectypes.Any) (*SignerData, error) // SigningComponents returns rosetta's components required to build a signable transaction SigningComponents(tx authsigning.Tx, metadata *ConstructionMetadata, rosPubKeys []*rosettatypes.PublicKey) (txBytes []byte, payloadsToSign []*rosettatypes.SigningPayload, err error) // Tx converts a tendermint transaction and tx result if provided to a rosetta tx @@ -780,3 +784,17 @@ func (c converter) SigningComponents(tx authsigning.Tx, metadata *ConstructionMe return } + +// SignerData converts the given any account to signer data +func (c converter) SignerData(anyAccount *codectypes.Any) (*SignerData, error) { + var acc auth.AccountI + err := c.ir.UnpackAny(anyAccount, &acc) + if err != nil { + return nil, crgerrs.WrapError(crgerrs.ErrCodec, err.Error()) + } + + return &SignerData{ + AccountNumber: acc.GetAccountNumber(), + Sequence: acc.GetSequence(), + }, nil +} From e2b1a01897beae2e81a77149ef6ce9cca5229efa Mon Sep 17 00:00:00 2001 From: Frojdi Dymylja Date: Fri, 5 Mar 2021 09:17:01 +0100 Subject: [PATCH 09/33] chore: reorder methods declaration --- server/rosetta/client_online.go | 72 ++++++++++++++++---------------- server/rosetta/client_v2_test.go | 48 --------------------- server/rosetta/converter_test.go | 15 ------- 3 files changed, 37 insertions(+), 98 deletions(-) delete mode 100644 server/rosetta/client_v2_test.go diff --git a/server/rosetta/client_online.go b/server/rosetta/client_online.go index f8135b9c9ff1..5ebd1f1c2fdb 100644 --- a/server/rosetta/client_online.go +++ b/server/rosetta/client_online.go @@ -91,6 +91,43 @@ func NewClient(cfg *Config) (*Client, error) { }, nil } +// Bootstrap is gonna connect the client to the endpoints +func (c *Client) Bootstrap() error { + grpcConn, err := grpc.Dial(c.config.GRPCEndpoint, grpc.WithInsecure()) + if err != nil { + return err + } + + tmRPC, err := http.New(c.config.TendermintRPC, tmWebsocketPath) + if err != nil { + return err + } + + authClient := auth.NewQueryClient(grpcConn) + bankClient := bank.NewQueryClient(grpcConn) + + c.auth = authClient + c.bank = bankClient + c.tmRPC = tmRPC + + return nil +} + +// Ready asserts if the client is ready or not +func (c *Client) Ready() error { + ctx, cancel := context.WithTimeout(context.Background(), defaultNodeTimeout) + defer cancel() + _, err := c.tmRPC.Health(ctx) + if err != nil { + return err + } + _, err = c.bank.TotalSupply(ctx, &bank.QueryTotalSupplyRequest{}) + if err != nil { + return err + } + return nil +} + func (c *Client) accountInfo(ctx context.Context, addr string, height *int64) (*SignerData, error) { if height != nil { strHeight := strconv.FormatInt(*height, 10) @@ -333,41 +370,6 @@ func (c *Client) PostTx(txBytes []byte) (*rosettatypes.TransactionIdentifier, ma }, nil } -func (c *Client) Ready() error { - ctx, cancel := context.WithTimeout(context.Background(), defaultNodeTimeout) - defer cancel() - _, err := c.tmRPC.Health(ctx) - if err != nil { - return err - } - _, err = c.bank.TotalSupply(ctx, &bank.QueryTotalSupplyRequest{}) - if err != nil { - return err - } - return nil -} - -func (c *Client) Bootstrap() error { - grpcConn, err := grpc.Dial(c.config.GRPCEndpoint, grpc.WithInsecure()) - if err != nil { - return err - } - - tmRPC, err := http.New(c.config.TendermintRPC, tmWebsocketPath) - if err != nil { - return err - } - - authClient := auth.NewQueryClient(grpcConn) - bankClient := bank.NewQueryClient(grpcConn) - - c.auth = authClient - c.bank = bankClient - c.tmRPC = tmRPC - - return nil -} - // construction endpoints // ConstructionMetadataFromOptions builds the metadata given the options diff --git a/server/rosetta/client_v2_test.go b/server/rosetta/client_v2_test.go deleted file mode 100644 index 477ff1a236b2..000000000000 --- a/server/rosetta/client_v2_test.go +++ /dev/null @@ -1,48 +0,0 @@ -package rosetta - -import ( - "context" - "testing" -) - -func TestClientV2(t *testing.T) { - cdc, ir := MakeCodec() - c, err := NewClient(&Config{ - Blockchain: "", - Network: "", - TendermintRPC: "tcp://localhost:26657", - GRPCEndpoint: "localhost:9090", - Addr: "", - Retries: 0, - Offline: false, - Codec: cdc, - InterfaceRegistry: ir, - }) - if err != nil { - t.Fatal(err) - } - - if err := c.Bootstrap(); err != nil { - t.Fatal(err) - } - - var h int64 = 3 - - blockTransactionsResponse, err := c.blockTxs(context.TODO(), &h) - if err != nil { - t.Fatal(err) - } - - for _, tx := range blockTransactionsResponse.Transactions { - t.Logf("hash: %s", tx.TransactionIdentifier.Hash) - for _, op := range tx.Operations { - t.Logf("\t name: %s", op.Type) - t.Logf("\t\t index: %d", op.OperationIdentifier.Index) - if op.Amount != nil { - t.Logf("\t\t coin change: %s%s", op.Amount.Value, op.Amount.Currency.Symbol) - } - t.Logf("\t\t address: %s", op.Account.Address) - t.Logf("\t\t meta: %#v", op.Metadata) - } - } -} diff --git a/server/rosetta/converter_test.go b/server/rosetta/converter_test.go index fff4f476f717..237e933f031e 100644 --- a/server/rosetta/converter_test.go +++ b/server/rosetta/converter_test.go @@ -1,7 +1,6 @@ package rosetta import ( - "encoding/hex" "testing" rosettatypes "github.com/coinbase/rosetta-sdk-go/types" @@ -108,17 +107,3 @@ func (s *ConverterTestSuite) TestMsgToMetaMetaToMsg() { func TestConverterTestSuite(t *testing.T) { suite.Run(t, new(ConverterTestSuite)) } - -func (s *ConverterTestSuite) TestX() { - const txRaw = "0a8e010a8b010a1c2f636f736d6f732e62616e6b2e763162657461312e4d736753656e64126b0a2d636f736d6f7331656e377a6574686b6c6c79307761386a7778777878727638396565386a383668656374747337122d636f736d6f73317377383670393076393076753875706d363478327173373068756663796330746d34766e37361a0b0a057374616b651202313812600a4c0a460a1f2f636f736d6f732e63727970746f2e736563703235366b312e5075624b657912230a21030c65f93f08cc27ee461ce1cd0a11647f0cfe852d044874d4d0905240a9787ec412020a0012100a0a0a057374616b651201311090a10f1a00" - - raw, _ := hex.DecodeString(txRaw) - cdc, _ := MakeCodec() - txConfig := authtx.NewTxConfig(cdc, authtx.DefaultSignModes) - - tx, err := txConfig.TxDecoder()(raw) - s.Require().NoError(err) - txB, err := txConfig.TxJSONEncoder()(tx) - s.Require().NoError(err) - s.T().Logf("%s", txB) -} From 5292379efe188e3d4a16ce4e8a652a6267138be6 Mon Sep 17 00:00:00 2001 From: Frojdi Dymylja Date: Fri, 5 Mar 2021 09:34:32 +0100 Subject: [PATCH 10/33] add: signed tx tests --- server/rosetta/converter.go | 7 ++++++- server/rosetta/converter_test.go | 28 ++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/server/rosetta/converter.go b/server/rosetta/converter.go index ef49dac6fe82..88075d4fc4df 100644 --- a/server/rosetta/converter.go +++ b/server/rosetta/converter.go @@ -651,6 +651,12 @@ func (c converter) SignedTx(txBytes []byte, signatures []*rosettatypes.Signature return nil, crgerrs.WrapError(crgerrs.ErrCodec, err.Error()) } + if len(notSignedSigs) != len(signatures) { + return nil, crgerrs.WrapError( + crgerrs.ErrInvalidTransaction, + fmt.Sprintf("expected transaction to have signers data matching the provided signatures: %d <-> %d", len(notSignedSigs), len(signatures))) + } + signedSigs := make([]signing.SignatureV2, len(notSignedSigs)) for i, signature := range signatures { // TODO(fdymylja): here we should check that the public key matches... @@ -776,7 +782,6 @@ func (c converter) SigningComponents(tx authsigning.Tx, metadata *ConstructionMe } // finally encode the tx - // encode tx txBytes, err = c.txEncode(builder.GetTx()) if err != nil { return nil, nil, crgerrs.WrapError(crgerrs.ErrCodec, err.Error()) diff --git a/server/rosetta/converter_test.go b/server/rosetta/converter_test.go index 237e933f031e..9dc07482b6d3 100644 --- a/server/rosetta/converter_test.go +++ b/server/rosetta/converter_test.go @@ -1,6 +1,8 @@ package rosetta import ( + "encoding/hex" + "encoding/json" "testing" rosettatypes "github.com/coinbase/rosetta-sdk-go/types" @@ -104,6 +106,32 @@ func (s *ConverterTestSuite) TestMsgToMetaMetaToMsg() { s.Require().Equal(msg, copyMsg) } +func (s *ConverterTestSuite) TestSignedTx() { + const unsignedTxHex = "0a8e010a8b010a1c2f636f736d6f732e62616e6b2e763162657461312e4d736753656e64126b0a2d636f736d6f733134376b6c68377468356a6b6a793361616a736a3272717668747668396d666465333777713567122d636f736d6f73316d6e7670386c786b616679346c787777617175356561653764787630647a36687767797436331a0b0a057374616b651202313612600a4c0a460a1f2f636f736d6f732e63727970746f2e736563703235366b312e5075624b657912230a21034c92046950c876f4a5cb6c7797d6eeb9ef80d67ced4d45fb62b1e859240ba9ad12020a0012100a0a0a057374616b651201311090a10f1a00" + unsignedTxBytes, err := hex.DecodeString(unsignedTxHex) + s.Require().NoError(err) + + s.Run("success", func() { + const payloadsJSON = `[{"hex_bytes":"82ccce81a3e4a7272249f0e25c3037a316ee2acce76eb0c25db00ef6634a4d57303b2420edfdb4c9a635ad8851fe5c7a9379b7bc2baadc7d74f7e76ac97459b5","signing_payload":{"address":"cosmos147klh7th5jkjy3aajsj2rqvhtvh9mfde37wq5g","hex_bytes":"ed574d84b095250280de38bf8c254e4a1f8755e5bd300b1f6ca2671688136ecc","account_identifier":{"address":"cosmos147klh7th5jkjy3aajsj2rqvhtvh9mfde37wq5g"},"signature_type":"ecdsa"},"public_key":{"hex_bytes":"034c92046950c876f4a5cb6c7797d6eeb9ef80d67ced4d45fb62b1e859240ba9ad","curve_type":"secp256k1"},"signature_type":"ecdsa"}]` + const expectedSignedTxHex = "0a8e010a8b010a1c2f636f736d6f732e62616e6b2e763162657461312e4d736753656e64126b0a2d636f736d6f733134376b6c68377468356a6b6a793361616a736a3272717668747668396d666465333777713567122d636f736d6f73316d6e7670386c786b616679346c787777617175356561653764787630647a36687767797436331a0b0a057374616b651202313612620a4e0a460a1f2f636f736d6f732e63727970746f2e736563703235366b312e5075624b657912230a21034c92046950c876f4a5cb6c7797d6eeb9ef80d67ced4d45fb62b1e859240ba9ad12040a02087f12100a0a0a057374616b651201311090a10f1a4082ccce81a3e4a7272249f0e25c3037a316ee2acce76eb0c25db00ef6634a4d57303b2420edfdb4c9a635ad8851fe5c7a9379b7bc2baadc7d74f7e76ac97459b5" + + var payloads []*rosettatypes.Signature + s.Require().NoError(json.Unmarshal([]byte(payloadsJSON), &payloads)) + + signedTx, err := s.c.ToSDK().SignedTx(unsignedTxBytes, payloads) + s.Require().NoError(err) + + signedTxHex := hex.EncodeToString(signedTx) + + s.Require().Equal(signedTxHex, expectedSignedTxHex) + }) + + s.Run("signers data and signing payloads mismatch", func() { + _, err := s.c.ToSDK().SignedTx(unsignedTxBytes, nil) + s.Require().ErrorIs(err, crgerrs.ErrInvalidTransaction) + }) +} + func TestConverterTestSuite(t *testing.T) { suite.Run(t, new(ConverterTestSuite)) } From 976f2e24a906a8f3385e02d03bd4d9445128bb48 Mon Sep 17 00:00:00 2001 From: Frojdi Dymylja Date: Fri, 5 Mar 2021 09:47:59 +0100 Subject: [PATCH 11/33] add: ops and signers test --- server/rosetta/converter_test.go | 59 ++++++++++++++++++++++++++++---- 1 file changed, 53 insertions(+), 6 deletions(-) diff --git a/server/rosetta/converter_test.go b/server/rosetta/converter_test.go index 9dc07482b6d3..2cb03de206af 100644 --- a/server/rosetta/converter_test.go +++ b/server/rosetta/converter_test.go @@ -5,6 +5,10 @@ import ( "encoding/json" "testing" + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/codec" + codectypes "github.com/cosmos/cosmos-sdk/codec/types" + rosettatypes "github.com/coinbase/rosetta-sdk-go/types" "github.com/stretchr/testify/suite" crgerrs "github.com/tendermint/cosmos-rosetta-gateway/errors" @@ -17,13 +21,32 @@ import ( type ConverterTestSuite struct { suite.Suite - c Converter + c Converter + unsignedTxBytes []byte + + util struct { + ir codectypes.InterfaceRegistry + cdc *codec.ProtoCodec + txConf client.TxConfig + } } func (s *ConverterTestSuite) SetupTest() { + // create an unsigned tx + const unsignedTxHex = "0a8e010a8b010a1c2f636f736d6f732e62616e6b2e763162657461312e4d736753656e64126b0a2d636f736d6f733134376b6c68377468356a6b6a793361616a736a3272717668747668396d666465333777713567122d636f736d6f73316d6e7670386c786b616679346c787777617175356561653764787630647a36687767797436331a0b0a057374616b651202313612600a4c0a460a1f2f636f736d6f732e63727970746f2e736563703235366b312e5075624b657912230a21034c92046950c876f4a5cb6c7797d6eeb9ef80d67ced4d45fb62b1e859240ba9ad12020a0012100a0a0a057374616b651201311090a10f1a00" + unsignedTxBytes, err := hex.DecodeString(unsignedTxHex) + s.Require().NoError(err) + s.unsignedTxBytes = unsignedTxBytes + // instantiate converter cdc, ir := MakeCodec() txConfig := authtx.NewTxConfig(cdc, authtx.DefaultSignModes) s.c = NewConverter(cdc, ir, txConfig) + // add utils + s.util = struct { + ir codectypes.InterfaceRegistry + cdc *codec.ProtoCodec + txConf client.TxConfig + }{ir: ir, cdc: cdc, txConf: txConfig} } func (s *ConverterTestSuite) TestFromRosettaOpsToTxSuccess() { @@ -107,9 +130,6 @@ func (s *ConverterTestSuite) TestMsgToMetaMetaToMsg() { } func (s *ConverterTestSuite) TestSignedTx() { - const unsignedTxHex = "0a8e010a8b010a1c2f636f736d6f732e62616e6b2e763162657461312e4d736753656e64126b0a2d636f736d6f733134376b6c68377468356a6b6a793361616a736a3272717668747668396d666465333777713567122d636f736d6f73316d6e7670386c786b616679346c787777617175356561653764787630647a36687767797436331a0b0a057374616b651202313612600a4c0a460a1f2f636f736d6f732e63727970746f2e736563703235366b312e5075624b657912230a21034c92046950c876f4a5cb6c7797d6eeb9ef80d67ced4d45fb62b1e859240ba9ad12020a0012100a0a0a057374616b651201311090a10f1a00" - unsignedTxBytes, err := hex.DecodeString(unsignedTxHex) - s.Require().NoError(err) s.Run("success", func() { const payloadsJSON = `[{"hex_bytes":"82ccce81a3e4a7272249f0e25c3037a316ee2acce76eb0c25db00ef6634a4d57303b2420edfdb4c9a635ad8851fe5c7a9379b7bc2baadc7d74f7e76ac97459b5","signing_payload":{"address":"cosmos147klh7th5jkjy3aajsj2rqvhtvh9mfde37wq5g","hex_bytes":"ed574d84b095250280de38bf8c254e4a1f8755e5bd300b1f6ca2671688136ecc","account_identifier":{"address":"cosmos147klh7th5jkjy3aajsj2rqvhtvh9mfde37wq5g"},"signature_type":"ecdsa"},"public_key":{"hex_bytes":"034c92046950c876f4a5cb6c7797d6eeb9ef80d67ced4d45fb62b1e859240ba9ad","curve_type":"secp256k1"},"signature_type":"ecdsa"}]` @@ -118,7 +138,7 @@ func (s *ConverterTestSuite) TestSignedTx() { var payloads []*rosettatypes.Signature s.Require().NoError(json.Unmarshal([]byte(payloadsJSON), &payloads)) - signedTx, err := s.c.ToSDK().SignedTx(unsignedTxBytes, payloads) + signedTx, err := s.c.ToSDK().SignedTx(s.unsignedTxBytes, payloads) s.Require().NoError(err) signedTxHex := hex.EncodeToString(signedTx) @@ -127,11 +147,38 @@ func (s *ConverterTestSuite) TestSignedTx() { }) s.Run("signers data and signing payloads mismatch", func() { - _, err := s.c.ToSDK().SignedTx(unsignedTxBytes, nil) + _, err := s.c.ToSDK().SignedTx(s.unsignedTxBytes, nil) s.Require().ErrorIs(err, crgerrs.ErrInvalidTransaction) }) } +func (s *ConverterTestSuite) TestOpsAndSigners() { + s.Run("success", func() { + addr1 := sdk.AccAddress("address1").String() + addr2 := sdk.AccAddress("address2").String() + + msg := &bank.MsgSend{ + FromAddress: addr1, + ToAddress: addr2, + Amount: sdk.NewCoins(sdk.NewInt64Coin("test", 10)), + } + + builder := s.util.txConf.NewTxBuilder() + s.Require().NoError(builder.SetMsgs(msg)) + + sdkTx := builder.GetTx() + txBytes, err := s.util.txConf.TxEncoder()(sdkTx) + s.Require().NoError(err) + + ops, signers, err := s.c.ToRosetta().OpsAndSigners(txBytes) + s.Require().NoError(err) + + s.Require().Equal(len(ops), len(sdkTx.GetMsgs())*len(sdkTx.GetSigners()), "operation number mismatch") + + s.Require().Equal(len(signers), len(sdkTx.GetSigners()), "signers number mismatch") + }) +} + func TestConverterTestSuite(t *testing.T) { suite.Run(t, new(ConverterTestSuite)) } From a489652724c8fcf227cc368516601d3b1a097ca2 Mon Sep 17 00:00:00 2001 From: Frojdi Dymylja Date: Fri, 5 Mar 2021 10:04:49 +0100 Subject: [PATCH 12/33] fix: begin and endblock tx conversions --- server/rosetta/converter.go | 6 ++++-- server/rosetta/converter_test.go | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/server/rosetta/converter.go b/server/rosetta/converter.go index 88075d4fc4df..c227875e4330 100644 --- a/server/rosetta/converter.go +++ b/server/rosetta/converter.go @@ -505,14 +505,16 @@ func AddOperationIndexes(msgOps []*rosettatypes.Operation, balanceOps []*rosetta // for endblock operations, it also serves the purpose of representing // part of the state changes happening at endblock level (balance ones) func (c converter) EndBlockTxHash(hash []byte) string { - return fmt.Sprintf("%X%X", EndBlockHashStart, hash) + final := append([]byte{EndBlockHashStart}, hash...) + return fmt.Sprintf("%X", final) } // beginBlockTxHash produces a mock beginblock hash that rosetta can query // for beginblock operations, it also serves the purpose of representing // part of the state changes happening at beginblock level (balance ones) func (c converter) BeginBlockTxHash(hash []byte) string { - return fmt.Sprintf("%X%X", BeginBlockHashStart, hash) + final := append([]byte{BeginBlockHashStart}, hash...) + return fmt.Sprintf("%X", final) } // HashToTxType takes the provided hash bytes from rosetta and discerns if they are diff --git a/server/rosetta/converter_test.go b/server/rosetta/converter_test.go index 2cb03de206af..951b477d7762 100644 --- a/server/rosetta/converter_test.go +++ b/server/rosetta/converter_test.go @@ -179,6 +179,38 @@ func (s *ConverterTestSuite) TestOpsAndSigners() { }) } +func (s *ConverterTestSuite) TestBeginEndBlockAndHashToTxType() { + const deliverTxHex = "5229A67AA008B5C5F1A0AEA77D4DEBE146297A30AAEF01777AF10FAD62DD36AB" + + deliverTxBytes, err := hex.DecodeString(deliverTxHex) + s.Require().NoError(err) + + endBlockTxHex := s.c.ToRosetta().EndBlockTxHash(deliverTxBytes) + beginBlockTxHex := s.c.ToRosetta().BeginBlockTxHash(deliverTxBytes) + + txType, hash := s.c.ToSDK().HashToTxType(deliverTxBytes) + + s.Require().Equal(DeliverTxTx, txType) + s.Require().Equal(deliverTxBytes, hash, "deliver tx hash should not change") + + endBlockTxBytes, err := hex.DecodeString(endBlockTxHex) + s.Require().NoError(err) + + txType, hash = s.c.ToSDK().HashToTxType(endBlockTxBytes) + + s.Require().Equal(EndBlockTx, txType) + s.Require().Equal(deliverTxBytes, hash, "end block tx hash should be equal to a block hash") + + beginBlockTxBytes, err := hex.DecodeString(beginBlockTxHex) + s.Require().NoError(err) + + txType, hash = s.c.ToSDK().HashToTxType(beginBlockTxBytes) + + s.Require().Equal(BeginBlockTx, txType) + s.Require().Equal(deliverTxBytes, hash, "begin block tx hash should be equal to a block hash") + +} + func TestConverterTestSuite(t *testing.T) { suite.Run(t, new(ConverterTestSuite)) } From 18bd9e3f69fd1629135421d1d8a95760cb2268bb Mon Sep 17 00:00:00 2001 From: Frojdi Dymylja Date: Fri, 5 Mar 2021 10:11:40 +0100 Subject: [PATCH 13/33] add: begin and endblock invalid tests --- server/rosetta/converter_test.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/server/rosetta/converter_test.go b/server/rosetta/converter_test.go index 951b477d7762..82b3383ad4d7 100644 --- a/server/rosetta/converter_test.go +++ b/server/rosetta/converter_test.go @@ -209,6 +209,14 @@ func (s *ConverterTestSuite) TestBeginEndBlockAndHashToTxType() { s.Require().Equal(BeginBlockTx, txType) s.Require().Equal(deliverTxBytes, hash, "begin block tx hash should be equal to a block hash") + txType, hash = s.c.ToSDK().HashToTxType([]byte("invalid")) + + s.Require().Equal(UnrecognizedTx, txType) + s.Require().Nil(hash) + + txType, hash = s.c.ToSDK().HashToTxType(append([]byte{0x3}, deliverTxBytes...)) + s.Require().Equal(UnrecognizedTx, txType) + s.Require().Nil(hash) } func TestConverterTestSuite(t *testing.T) { From a32c005e7bab8697a529d99959c1defb0874d9ee Mon Sep 17 00:00:00 2001 From: Frojdi Dymylja Date: Fri, 5 Mar 2021 10:56:57 +0100 Subject: [PATCH 14/33] add: balance and signing components tests --- server/rosetta/converter_test.go | 129 +++++++++++++++++++++++++++++++ 1 file changed, 129 insertions(+) diff --git a/server/rosetta/converter_test.go b/server/rosetta/converter_test.go index 82b3383ad4d7..8bc4dbd7991f 100644 --- a/server/rosetta/converter_test.go +++ b/server/rosetta/converter_test.go @@ -5,6 +5,10 @@ import ( "encoding/json" "testing" + abci "github.com/tendermint/tendermint/abci/types" + + authsigning "github.com/cosmos/cosmos-sdk/x/auth/signing" + "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/codec" codectypes "github.com/cosmos/cosmos-sdk/codec/types" @@ -23,6 +27,7 @@ type ConverterTestSuite struct { c Converter unsignedTxBytes []byte + unsignedTx authsigning.Tx util struct { ir codectypes.InterfaceRegistry @@ -47,6 +52,13 @@ func (s *ConverterTestSuite) SetupTest() { cdc *codec.ProtoCodec txConf client.TxConfig }{ir: ir, cdc: cdc, txConf: txConfig} + // add authsigning tx + sdkTx, err := txConfig.TxDecoder()(unsignedTxBytes) + s.Require().NoError(err) + builder, err := txConfig.WrapTxBuilder(sdkTx) + s.Require().NoError(err) + + s.unsignedTx = builder.GetTx() } func (s *ConverterTestSuite) TestFromRosettaOpsToTxSuccess() { @@ -219,6 +231,123 @@ func (s *ConverterTestSuite) TestBeginEndBlockAndHashToTxType() { s.Require().Nil(hash) } +func (s *ConverterTestSuite) TestSigningComponents() { + s.Run("invalid metadata coins", func() { + _, _, err := s.c.ToRosetta().SigningComponents(nil, &ConstructionMetadata{GasPrice: "invalid"}, nil) + s.Require().ErrorIs(err, crgerrs.ErrBadArgument) + }) + + s.Run("length signers data does not match signers", func() { + _, _, err := s.c.ToRosetta().SigningComponents(s.unsignedTx, &ConstructionMetadata{GasPrice: "10stake"}, nil) + s.Require().ErrorIs(err, crgerrs.ErrBadArgument) + }) + + s.Run("length pub keys does not match signers", func() { + _, _, err := s.c.ToRosetta().SigningComponents( + s.unsignedTx, + &ConstructionMetadata{GasPrice: "10stake", SignersData: []*SignerData{ + { + AccountNumber: 0, + Sequence: 0, + }, + }}, + nil) + s.Require().ErrorIs(err, crgerrs.ErrBadArgument) + }) + + s.Run("ros pub key is valid but not the one we expect", func() { + validButUnexpected, err := hex.DecodeString("030da9096a40eb1d6c25f1e26e9cbf8941fc84b8f4dc509c8df5e62a29ab8f2415") + s.Require().NoError(err) + + _, _, err = s.c.ToRosetta().SigningComponents( + s.unsignedTx, + &ConstructionMetadata{GasPrice: "10stake", SignersData: []*SignerData{ + { + AccountNumber: 0, + Sequence: 0, + }, + }}, + []*rosettatypes.PublicKey{ + { + Bytes: validButUnexpected, + CurveType: rosettatypes.Secp256k1, + }, + }) + s.Require().ErrorIs(err, crgerrs.ErrBadArgument) + }) + + s.Run("success", func() { + expectedPubKey, err := hex.DecodeString("034c92046950c876f4a5cb6c7797d6eeb9ef80d67ced4d45fb62b1e859240ba9ad") + s.Require().NoError(err) + + _, _, err = s.c.ToRosetta().SigningComponents( + s.unsignedTx, + &ConstructionMetadata{GasPrice: "10stake", SignersData: []*SignerData{ + { + AccountNumber: 0, + Sequence: 0, + }, + }}, + []*rosettatypes.PublicKey{ + { + Bytes: expectedPubKey, + CurveType: rosettatypes.Secp256k1, + }, + }) + s.Require().NoError(err) + }) + +} + +func (s *ConverterTestSuite) TestBalanceOps() { + s.Run("not a balance op", func() { + notBalanceOp := abci.Event{ + Type: "not-a-balance-op", + } + + ops := s.c.ToRosetta().BalanceOps("", []abci.Event{notBalanceOp}) + s.Len(ops, 0, "expected no balance ops") + }) + + s.Run("multiple balance ops from 2 multicoins event", func() { + subBalanceOp := bank.NewCoinSpentEvent( + sdk.AccAddress("test"), + sdk.NewCoins(sdk.NewInt64Coin("test", 10), sdk.NewInt64Coin("utxo", 10)), + ) + + addBalanceOp := bank.NewCoinReceivedEvent( + sdk.AccAddress("test"), + sdk.NewCoins(sdk.NewInt64Coin("test", 10), sdk.NewInt64Coin("utxo", 10)), + ) + + ops := s.c.ToRosetta().BalanceOps("", []abci.Event{(abci.Event)(subBalanceOp), (abci.Event)(addBalanceOp)}) + s.Len(ops, 4) + }) + + s.Run("spec broken", func() { + s.Require().Panics(func() { + specBrokenSub := abci.Event{ + Type: bank.EventTypeCoinSpent, + } + _ = s.c.ToRosetta().BalanceOps("", []abci.Event{specBrokenSub}) + }) + + s.Require().Panics(func() { + specBrokenSub := abci.Event{ + Type: bank.EventTypeCoinBurn, + } + _ = s.c.ToRosetta().BalanceOps("", []abci.Event{specBrokenSub}) + }) + + s.Require().Panics(func() { + specBrokenSub := abci.Event{ + Type: bank.EventTypeCoinReceived, + } + _ = s.c.ToRosetta().BalanceOps("", []abci.Event{specBrokenSub}) + }) + }) +} + func TestConverterTestSuite(t *testing.T) { suite.Run(t, new(ConverterTestSuite)) } From 9ad9d28c80e2d3abdde74618f2217ca29bc178ae Mon Sep 17 00:00:00 2001 From: Frojdi Dymylja Date: Fri, 5 Mar 2021 10:58:31 +0100 Subject: [PATCH 15/33] change: remove staking tests --- contrib/rosetta/configuration/run_tests.sh | 2 - contrib/rosetta/configuration/staking.json | 30 ----- contrib/rosetta/configuration/staking.ros | 147 --------------------- 3 files changed, 179 deletions(-) delete mode 100644 contrib/rosetta/configuration/staking.json delete mode 100644 contrib/rosetta/configuration/staking.ros diff --git a/contrib/rosetta/configuration/run_tests.sh b/contrib/rosetta/configuration/run_tests.sh index cd7af92acda2..d65d50775f05 100755 --- a/contrib/rosetta/configuration/run_tests.sh +++ b/contrib/rosetta/configuration/run_tests.sh @@ -25,5 +25,3 @@ rosetta-cli check:data --configuration-file ./config/rosetta.json echo "checking construction API" rosetta-cli check:construction --configuration-file ./config/rosetta.json -echo "checking staking API" -rosetta-cli check:construction --configuration-file ./config/staking.json diff --git a/contrib/rosetta/configuration/staking.json b/contrib/rosetta/configuration/staking.json deleted file mode 100644 index 9c5e5da3ba46..000000000000 --- a/contrib/rosetta/configuration/staking.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "network": { - "blockchain": "app", - "network": "network" - }, - "online_url": "http://rosetta:8080", - "data_directory": "", - "http_timeout": 300, - "max_retries": 5, - "retry_elapsed_time": 0, - "max_online_connections": 0, - "max_sync_concurrency": 0, - "tip_delay": 60, - "log_configuration": true, - "construction": { - "offline_url": "http://rosetta:8080", - "max_offline_connections": 0, - "stale_depth": 0, - "broadcast_limit": 0, - "ignore_broadcast_failures": false, - "clear_broadcasts": false, - "broadcast_behind_tip": false, - "block_broadcast_limit": 0, - "rebroadcast_all": false, - "constructor_dsl_file": "staking.ros", - "end_conditions": { - "staking": 3 - } - } -} \ No newline at end of file diff --git a/contrib/rosetta/configuration/staking.ros b/contrib/rosetta/configuration/staking.ros deleted file mode 100644 index fba5e79a73b1..000000000000 --- a/contrib/rosetta/configuration/staking.ros +++ /dev/null @@ -1,147 +0,0 @@ -request_funds(1){ - find_account{ - currency = {"symbol":"stake", "decimals":0}; - random_account = find_balance({ - "minimum_balance":{ - "value": "0", - "currency": {{currency}} - }, - "create_limit":1 - }); - }, - send_funds{ - account_identifier = {{random_account.account_identifier}}; - address = {{account_identifier.address}}; - idk = http_request({ - "method": "POST", - "url": "http:\/\/faucet:8000", - "timeout": 10, - "body": {{random_account.account_identifier.address}} - }); - }, - // Create a separate scenario to request funds so that - // the address we are using to request funds does not - // get rolled back if funds do not yet exist. - request{ - loaded_account = find_balance({ - "account_identifier": {{random_account.account_identifier}}, - "minimum_balance":{ - "value": "100", - "currency": {{currency}} - } - }); - } -} -create_account(1){ - create{ - network = {"network":"network", "blockchain":"app"}; - key = generate_key({"curve_type": "secp256k1"}); - account = derive({ - "network_identifier": {{network}}, - "public_key": {{key.public_key}} - }); - // If the account is not saved, the key will be lost! - save_account({ - "account_identifier": {{account.account_identifier}}, - "keypair": {{key}} - }); - } -} - -staking(1){ - stake{ - stake.network = {"network":"network", "blockchain":"app"}; - currency = {"symbol":"stake", "decimals":0}; - sender = find_balance({ - "minimum_balance":{ - "value": "100", - "currency": {{currency}} - } - }); - // Set the recipient_amount as some value <= sender.balance-max_fee - max_fee = "0"; - fee_amount = "1"; - fee_value = 0 - {{fee_amount}}; - available_amount = {{sender.balance.value}} - {{max_fee}}; - recipient_amount = "1"; - print_message({"recipient_amount":{{recipient_amount}}}); - // Find recipient and construct operations - recipient = {{sender.account_identifier}}; - sender_amount = 0 - {{recipient_amount}}; - stake.confirmation_depth = "1"; - stake.operations = [ - { - "operation_identifier":{"index":0}, - "type":"fee", - "account":{{sender.account_identifier}}, - "amount":{ - "value":{{fee_value}}, - "currency":{{currency}} - } - }, - { - "operation_identifier":{"index":1}, - "type":"cosmos.staking.v1beta1.MsgDelegate", - "account":{{sender.account_identifier}}, - "amount":{ - "value":{{sender_amount}}, - "currency":{{currency}} - } - }, - { - "operation_identifier":{"index":2}, - "type":"cosmos.staking.v1beta1.MsgDelegate", - "account": { - "address": "staking_account", - "sub_account": { - "address" : "cosmosvaloper19g9cm8ymzchq2qkcdv3zgqtwayj9asv3jxcps8" - } - }, - "amount":{ - "value":{{recipient_amount}}, - "currency":{{currency}} - } - } - ]; - }, - undelegate{ - print_message({"undelegate":{{sender}}}); - - undelegate.network = {"network":"network", "blockchain":"app"}; - undelegate.confirmation_depth = "1"; - undelegate.operations = [ - { - "operation_identifier":{"index":0}, - "type":"fee", - "account":{{sender.account_identifier}}, - "amount":{ - "value":{{fee_value}}, - "currency":{{currency}} - } - }, - { - "operation_identifier":{"index":1}, - "type":"cosmos.staking.v1beta1.MsgUndelegate", - "account":{{sender.account_identifier}}, - "amount":{ - "value":{{recipient_amount}}, - "currency":{{currency}} - } - }, - { - "operation_identifier":{"index":2}, - "type":"cosmos.staking.v1beta1.MsgUndelegate", - "account": { - "address": "staking_account", - "sub_account": { - "address" : "cosmosvaloper19g9cm8ymzchq2qkcdv3zgqtwayj9asv3jxcps8" - } - }, - "amount":{ - "value":{{sender_amount}}, - "currency":{{currency}} - } - } - ]; - } -} From 7375263b381c30e43e4c30508a617138fb370dbc Mon Sep 17 00:00:00 2001 From: Frojdi Dymylja Date: Fri, 5 Mar 2021 11:16:13 +0100 Subject: [PATCH 16/33] chore: lint --- server/rosetta/client_online.go | 6 ++++-- server/rosetta/converter.go | 17 ++++++++--------- server/rosetta/util.go | 3 ++- 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/server/rosetta/client_online.go b/server/rosetta/client_online.go index 5ebd1f1c2fdb..9e18cdeb6182 100644 --- a/server/rosetta/client_online.go +++ b/server/rosetta/client_online.go @@ -142,7 +142,9 @@ func (c *Client) accountInfo(ctx context.Context, addr string, height *int64) (* } signerData, err := c.converter.ToRosetta().SignerData(accountInfo.Account) - + if err != nil { + return nil, err + } return signerData, nil } @@ -305,7 +307,7 @@ func (c *Client) GetUnconfirmedTx(ctx context.Context, hash string) (*rosettatyp default: return nil, crgerrs.WrapError(crgerrs.ErrBadArgument, fmt.Sprintf("unrecognized tx size: %d", len(hashAsBytes))) case BeginEndBlockTxSize: - return nil, crgerrs.WrapError(crgerrs.ErrBadArgument, fmt.Sprintf("endblock and begin block txs cannot be unconfirmed")) + return nil, crgerrs.WrapError(crgerrs.ErrBadArgument, "endblock and begin block txs cannot be unconfirmed") case DeliverTxSize: break } diff --git a/server/rosetta/converter.go b/server/rosetta/converter.go index c227875e4330..ef9f66f089ec 100644 --- a/server/rosetta/converter.go +++ b/server/rosetta/converter.go @@ -113,14 +113,10 @@ type converter struct { func NewConverter(cdc *codec.ProtoCodec, ir codectypes.InterfaceRegistry, cfg sdkclient.TxConfig) Converter { return converter{ - newTxBuilder: func() sdkclient.TxBuilder { - return cfg.NewTxBuilder() - }, - txBuilderFromTx: func(tx sdk.Tx) (sdkclient.TxBuilder, error) { - return cfg.WrapTxBuilder(tx) - }, - txDecode: cfg.TxDecoder(), - txEncode: cfg.TxEncoder(), + newTxBuilder: cfg.NewTxBuilder, + txBuilderFromTx: cfg.WrapTxBuilder, + txDecode: cfg.TxDecoder(), + txEncode: cfg.TxEncoder(), bytesToSign: func(tx authsigning.Tx, signerData authsigning.SignerData) (b []byte, err error) { bytesToSign, err := cfg.SignModeHandler().GetSignBytes(signing.SignMode_SIGN_MODE_LEGACY_AMINO_JSON, signerData, tx) if err != nil { @@ -316,6 +312,8 @@ func (c converter) Tx(rawTx tmtypes.Tx, txResult *abci.ResponseDeliverTx) (*rose } // get operations from msgs msgs := tx.GetMsgs() + + // nolint: prealloc var rawTxOps []*rosettatypes.Operation for _, msg := range msgs { ops, err := c.Ops(status, msg) @@ -342,6 +340,7 @@ func (c converter) Tx(rawTx tmtypes.Tx, txResult *abci.ResponseDeliverTx) (*rose } func (c converter) BalanceOps(status string, events []abci.Event) []*rosettatypes.Operation { + // nolint: prealloc var ops []*rosettatypes.Operation for _, e := range events { @@ -789,7 +788,7 @@ func (c converter) SigningComponents(tx authsigning.Tx, metadata *ConstructionMe return nil, nil, crgerrs.WrapError(crgerrs.ErrCodec, err.Error()) } - return + return txBytes, payloadsToSign, nil } // SignerData converts the given any account to signer data diff --git a/server/rosetta/util.go b/server/rosetta/util.go index 42861a0991dc..43626b5ed351 100644 --- a/server/rosetta/util.go +++ b/server/rosetta/util.go @@ -2,8 +2,9 @@ package rosetta import ( "encoding/json" - crgerrs "github.com/tendermint/cosmos-rosetta-gateway/errors" "time" + + crgerrs "github.com/tendermint/cosmos-rosetta-gateway/errors" ) // timeToMilliseconds converts time to milliseconds timestamp From 2246c6ca267ec5b1d8a02dd2a403fe2c1b2633b7 Mon Sep 17 00:00:00 2001 From: Frojdi Dymylja Date: Fri, 5 Mar 2021 11:27:01 +0100 Subject: [PATCH 17/33] chore: lint --- server/rosetta/converter.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/rosetta/converter.go b/server/rosetta/converter.go index ef9f66f089ec..784135faba8f 100644 --- a/server/rosetta/converter.go +++ b/server/rosetta/converter.go @@ -313,7 +313,7 @@ func (c converter) Tx(rawTx tmtypes.Tx, txResult *abci.ResponseDeliverTx) (*rose // get operations from msgs msgs := tx.GetMsgs() - // nolint: prealloc + // nolint: prealloc, nolintlint var rawTxOps []*rosettatypes.Operation for _, msg := range msgs { ops, err := c.Ops(status, msg) @@ -340,7 +340,7 @@ func (c converter) Tx(rawTx tmtypes.Tx, txResult *abci.ResponseDeliverTx) (*rose } func (c converter) BalanceOps(status string, events []abci.Event) []*rosettatypes.Operation { - // nolint: prealloc + // nolint: prealloc, nolintlint var ops []*rosettatypes.Operation for _, e := range events { From 4377a09f4dd79b12c70f225f372cdc29d6bd6b72 Mon Sep 17 00:00:00 2001 From: Frojdi Dymylja Date: Fri, 5 Mar 2021 11:29:54 +0100 Subject: [PATCH 18/33] revert: makefile rosetta test --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index dbdffdd25cf7..66065b864a23 100644 --- a/Makefile +++ b/Makefile @@ -315,7 +315,7 @@ test-cover: test-rosetta: docker build -t rosetta-ci:latest -f contrib/rosetta/node/Dockerfile . - docker-compose -f contrib/rosetta/docker-compose.yaml up + docker-compose -f contrib/rosetta/docker-compose.yaml up --abort-on-container-exit --exit-code-from test_rosetta --build .PHONY: test-rosetta benchmark: From a1f82bcbda906cacdb7a428bda54b118dc7151ad Mon Sep 17 00:00:00 2001 From: Frojdi Dymylja Date: Fri, 5 Mar 2021 11:33:39 +0100 Subject: [PATCH 19/33] chore: lint again --- server/rosetta/converter.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/server/rosetta/converter.go b/server/rosetta/converter.go index 784135faba8f..729fb9253c01 100644 --- a/server/rosetta/converter.go +++ b/server/rosetta/converter.go @@ -313,7 +313,8 @@ func (c converter) Tx(rawTx tmtypes.Tx, txResult *abci.ResponseDeliverTx) (*rose // get operations from msgs msgs := tx.GetMsgs() - // nolint: prealloc, nolintlint + // nolint: nolintlint + // nolint: prealloc var rawTxOps []*rosettatypes.Operation for _, msg := range msgs { ops, err := c.Ops(status, msg) @@ -340,7 +341,8 @@ func (c converter) Tx(rawTx tmtypes.Tx, txResult *abci.ResponseDeliverTx) (*rose } func (c converter) BalanceOps(status string, events []abci.Event) []*rosettatypes.Operation { - // nolint: prealloc, nolintlint + // nolint: nolintlint + // nolint: prealloc var ops []*rosettatypes.Operation for _, e := range events { From 89de69c50c05fd942ec618188d29061b1d0ce74b Mon Sep 17 00:00:00 2001 From: Frojdi Dymylja Date: Fri, 5 Mar 2021 11:35:55 +0100 Subject: [PATCH 20/33] chore: move tests to package based ones --- server/rosetta/converter_test.go | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/server/rosetta/converter_test.go b/server/rosetta/converter_test.go index 8bc4dbd7991f..3ac0eedf007f 100644 --- a/server/rosetta/converter_test.go +++ b/server/rosetta/converter_test.go @@ -1,10 +1,12 @@ -package rosetta +package rosetta_test import ( "encoding/hex" "encoding/json" "testing" + "github.com/cosmos/cosmos-sdk/server/rosetta" + abci "github.com/tendermint/tendermint/abci/types" authsigning "github.com/cosmos/cosmos-sdk/x/auth/signing" @@ -25,7 +27,7 @@ import ( type ConverterTestSuite struct { suite.Suite - c Converter + c rosetta.Converter unsignedTxBytes []byte unsignedTx authsigning.Tx @@ -43,9 +45,9 @@ func (s *ConverterTestSuite) SetupTest() { s.Require().NoError(err) s.unsignedTxBytes = unsignedTxBytes // instantiate converter - cdc, ir := MakeCodec() + cdc, ir := rosetta.MakeCodec() txConfig := authtx.NewTxConfig(cdc, authtx.DefaultSignModes) - s.c = NewConverter(cdc, ir, txConfig) + s.c = rosetta.NewConverter(cdc, ir, txConfig) // add utils s.util = struct { ir codectypes.InterfaceRegistry @@ -202,7 +204,7 @@ func (s *ConverterTestSuite) TestBeginEndBlockAndHashToTxType() { txType, hash := s.c.ToSDK().HashToTxType(deliverTxBytes) - s.Require().Equal(DeliverTxTx, txType) + s.Require().Equal(rosetta.DeliverTxTx, txType) s.Require().Equal(deliverTxBytes, hash, "deliver tx hash should not change") endBlockTxBytes, err := hex.DecodeString(endBlockTxHex) @@ -210,7 +212,7 @@ func (s *ConverterTestSuite) TestBeginEndBlockAndHashToTxType() { txType, hash = s.c.ToSDK().HashToTxType(endBlockTxBytes) - s.Require().Equal(EndBlockTx, txType) + s.Require().Equal(rosetta.EndBlockTx, txType) s.Require().Equal(deliverTxBytes, hash, "end block tx hash should be equal to a block hash") beginBlockTxBytes, err := hex.DecodeString(beginBlockTxHex) @@ -218,34 +220,34 @@ func (s *ConverterTestSuite) TestBeginEndBlockAndHashToTxType() { txType, hash = s.c.ToSDK().HashToTxType(beginBlockTxBytes) - s.Require().Equal(BeginBlockTx, txType) + s.Require().Equal(rosetta.BeginBlockTx, txType) s.Require().Equal(deliverTxBytes, hash, "begin block tx hash should be equal to a block hash") txType, hash = s.c.ToSDK().HashToTxType([]byte("invalid")) - s.Require().Equal(UnrecognizedTx, txType) + s.Require().Equal(rosetta.UnrecognizedTx, txType) s.Require().Nil(hash) txType, hash = s.c.ToSDK().HashToTxType(append([]byte{0x3}, deliverTxBytes...)) - s.Require().Equal(UnrecognizedTx, txType) + s.Require().Equal(rosetta.UnrecognizedTx, txType) s.Require().Nil(hash) } func (s *ConverterTestSuite) TestSigningComponents() { s.Run("invalid metadata coins", func() { - _, _, err := s.c.ToRosetta().SigningComponents(nil, &ConstructionMetadata{GasPrice: "invalid"}, nil) + _, _, err := s.c.ToRosetta().SigningComponents(nil, &rosetta.ConstructionMetadata{GasPrice: "invalid"}, nil) s.Require().ErrorIs(err, crgerrs.ErrBadArgument) }) s.Run("length signers data does not match signers", func() { - _, _, err := s.c.ToRosetta().SigningComponents(s.unsignedTx, &ConstructionMetadata{GasPrice: "10stake"}, nil) + _, _, err := s.c.ToRosetta().SigningComponents(s.unsignedTx, &rosetta.ConstructionMetadata{GasPrice: "10stake"}, nil) s.Require().ErrorIs(err, crgerrs.ErrBadArgument) }) s.Run("length pub keys does not match signers", func() { _, _, err := s.c.ToRosetta().SigningComponents( s.unsignedTx, - &ConstructionMetadata{GasPrice: "10stake", SignersData: []*SignerData{ + &rosetta.ConstructionMetadata{GasPrice: "10stake", SignersData: []*rosetta.SignerData{ { AccountNumber: 0, Sequence: 0, @@ -261,7 +263,7 @@ func (s *ConverterTestSuite) TestSigningComponents() { _, _, err = s.c.ToRosetta().SigningComponents( s.unsignedTx, - &ConstructionMetadata{GasPrice: "10stake", SignersData: []*SignerData{ + &rosetta.ConstructionMetadata{GasPrice: "10stake", SignersData: []*rosetta.SignerData{ { AccountNumber: 0, Sequence: 0, @@ -282,7 +284,7 @@ func (s *ConverterTestSuite) TestSigningComponents() { _, _, err = s.c.ToRosetta().SigningComponents( s.unsignedTx, - &ConstructionMetadata{GasPrice: "10stake", SignersData: []*SignerData{ + &rosetta.ConstructionMetadata{GasPrice: "10stake", SignersData: []*rosetta.SignerData{ { AccountNumber: 0, Sequence: 0, From 687aef2f8c7cd006255748a9c8e3363c54cc3ec0 Mon Sep 17 00:00:00 2001 From: Frojdi Dymylja Date: Fri, 5 Mar 2021 11:38:13 +0100 Subject: [PATCH 21/33] chore: lint --- server/rosetta/converter.go | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/server/rosetta/converter.go b/server/rosetta/converter.go index 729fb9253c01..eef44d6dae76 100644 --- a/server/rosetta/converter.go +++ b/server/rosetta/converter.go @@ -313,9 +313,7 @@ func (c converter) Tx(rawTx tmtypes.Tx, txResult *abci.ResponseDeliverTx) (*rose // get operations from msgs msgs := tx.GetMsgs() - // nolint: nolintlint - // nolint: prealloc - var rawTxOps []*rosettatypes.Operation + var rawTxOps []*rosettatypes.Operation // nolint: prealloc for _, msg := range msgs { ops, err := c.Ops(status, msg) if err != nil { @@ -341,9 +339,7 @@ func (c converter) Tx(rawTx tmtypes.Tx, txResult *abci.ResponseDeliverTx) (*rose } func (c converter) BalanceOps(status string, events []abci.Event) []*rosettatypes.Operation { - // nolint: nolintlint - // nolint: prealloc - var ops []*rosettatypes.Operation + var ops []*rosettatypes.Operation // nolint: prealloc for _, e := range events { balanceOps, ok := sdkEventToBalanceOperations(status, e) From 8a9a1406f73b292c5f7c1f864b5b4fee4e808dbd Mon Sep 17 00:00:00 2001 From: Frojdi Dymylja Date: Fri, 5 Mar 2021 11:41:54 +0100 Subject: [PATCH 22/33] chore: lint --- server/rosetta/converter.go | 4 ++-- x/staking/types/msg.go | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/server/rosetta/converter.go b/server/rosetta/converter.go index eef44d6dae76..4d1a9c2f33c4 100644 --- a/server/rosetta/converter.go +++ b/server/rosetta/converter.go @@ -313,7 +313,7 @@ func (c converter) Tx(rawTx tmtypes.Tx, txResult *abci.ResponseDeliverTx) (*rose // get operations from msgs msgs := tx.GetMsgs() - var rawTxOps []*rosettatypes.Operation // nolint: prealloc + var rawTxOps []*rosettatypes.Operation for _, msg := range msgs { ops, err := c.Ops(status, msg) if err != nil { @@ -339,7 +339,7 @@ func (c converter) Tx(rawTx tmtypes.Tx, txResult *abci.ResponseDeliverTx) (*rose } func (c converter) BalanceOps(status string, events []abci.Event) []*rosettatypes.Operation { - var ops []*rosettatypes.Operation // nolint: prealloc + var ops []*rosettatypes.Operation for _, e := range events { balanceOps, ok := sdkEventToBalanceOperations(status, e) diff --git a/x/staking/types/msg.go b/x/staking/types/msg.go index f6d8b5bccc8b..e397046e5779 100644 --- a/x/staking/types/msg.go +++ b/x/staking/types/msg.go @@ -2,6 +2,7 @@ package types import ( "bytes" + codectypes "github.com/cosmos/cosmos-sdk/codec/types" cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" sdk "github.com/cosmos/cosmos-sdk/types" From bf428f4cfc2dd23edbce5b12cdc582c7cf25e721 Mon Sep 17 00:00:00 2001 From: Frojdi Dymylja Date: Fri, 5 Mar 2021 12:24:28 +0100 Subject: [PATCH 23/33] chore: cleanup ci --- contrib/rosetta/configuration/bootstrap.json | 2 +- contrib/rosetta/configuration/run_tests.sh | 10 ---------- contrib/rosetta/node/data.tar.gz | Bin 36798 -> 35433 bytes 3 files changed, 1 insertion(+), 11 deletions(-) diff --git a/contrib/rosetta/configuration/bootstrap.json b/contrib/rosetta/configuration/bootstrap.json index 401bc4551253..f75c7ec145bb 100644 --- a/contrib/rosetta/configuration/bootstrap.json +++ b/contrib/rosetta/configuration/bootstrap.json @@ -1,7 +1,7 @@ [ { "account_identifier": { - "address":"cosmos19g9cm8ymzchq2qkcdv3zgqtwayj9asv3hjv5u5" + "address":"cosmos1ujtnemf6jmfm995j000qdry064n5lq854gfe3j" }, "currency":{ "symbol":"stake", diff --git a/contrib/rosetta/configuration/run_tests.sh b/contrib/rosetta/configuration/run_tests.sh index d65d50775f05..c53f89ff88a4 100755 --- a/contrib/rosetta/configuration/run_tests.sh +++ b/contrib/rosetta/configuration/run_tests.sh @@ -2,16 +2,6 @@ set -e -addr="abcd" - -send_tx() { - echo '12345678' | simd tx bank send $addr "$1" "$2" -} - -detect_account() { - line=$1 -} - wait_for_rosetta() { timeout 30 sh -c 'until nc -z $0 $1; do sleep 1; done' rosetta 8080 } diff --git a/contrib/rosetta/node/data.tar.gz b/contrib/rosetta/node/data.tar.gz index 721cb90aaa1bcdf2fe9251a24cc72c496eaa4c06..987bb88b33acaa6ad7324d25828ba755dd9bad29 100644 GIT binary patch literal 35433 zcmV(}K+wM*iwFP!000001MFN0T#R`ipJ!&&^ay!e8N42|&bFPN=b2|tTB(p&N5xv! zGRIRU)nsN$xt3KFLhEiLYgsIts}!kUb)Yq8{bx8(iLOw-n+Mr(wR`T6LX zdVcf#?%(%$p8ug%skAO!jWSpo8tOuOGlT&WVwnDj)jsM!<0M94B#sM(JQ{`*7&xH( zH>6Ju3e#w%Y80iZfg|eUGk5+yC3W@qE2LWK8%Tl%#Gk+jTo-?#KxmDBV^Hn*Q{hSl z6(Xm&3Ymp+K_7oU{}t<>1o7wN0=_l=jY0Lt-(#eYPyeBQuPH+pU;#wm~FkHN1mqv8FZFCeV_zfs6s{0CY< zhPn9jNxo_P`PTm5IOJNB(9eJ~_|B;AJq+4&q@PDG)Pw`ZaS4G*2oaaylOS3sxH)tS zq5`N81v(%nZD?5V0A&z``sSqRj=svN6go5~t$&DC9f^A6q`6BKhR^Q$&+b7g`6P5O zm6JA@itMjetJJ7xPMW7$6&gxaG7iZ}8=}J|bVN>?FEtqmK>c#k{FK2|{~&3ohEn{a z5f2A>gP{tUn<|tFL3KZrAptec^GN|3PN_9YRfrpiyjG|4*l{{_F?ZcjuKTsxH%LB- zi*;8Fl!hn)80z>5bRb}_$wcz1#41!Flukt+#>Iq~Fj(_0KUCGOf0-^M8epo$-T>)p z{MFz8@bvZ1$F1*w8iUNm-x655p!=%wj;#~QN{_Bq?d{qlxnGImyQM=mEMMIr!Y6&C z#4Lw9dLp^Ql@7fJMRj*Q(qYZw^=nrD35|>Wxb1Q5jtxe(D46DLmcs{S&Wg{NQP{KZ zl+4Wdf!e z_aO+7ftZ!yaE8vd#@90O_3l>G-WP!v8G9lh6!iF2h{xpb>P$s3 zpH3SvwG$1pscsOgha_JeBr#c#12p23zi%#}L$)S8i;!SXm`ENIj$_o!X*#Edl1>(L z3xO(|#``WEhqW)*b#>l1a729W`3UbxWfk|<2tw!l9$NCvyHgr13%j&Pz2@CHWx!DW zl6c1RK4lN+saFH1e>`9(KR$?`u99x^)vpbV|?<{X0JMXU2Whv_P}US<)h% zeiGh(wD;(^^!BS*A>mOEAc$qst%*zeZawna;V!<9J1lS=8pm?6`ETI3Z#E?^=8S57 zQj*YS+^>^!3^tpS;gH8TK z@W%uSnLx@{hzO}rEEo9`1Ydwra*8JvOL0D5A(shpIROzTQdVe>fzD^r^xxaI{^?K0 z76Y7WYKl_?Fo@2!SdG+c&|JMicguD?p>y1>_PQ(HSiagm=_9XQoey{%n;EcT$imiJ zx|L~GuHx*!uyn)XXXOsQkxxF(iv4`V1G^>{dbk%vsKr^1--eU71Nt;u>DZkDIAvcC zPSNjN-gojB2Gq<1r+5-8q(ome#G!|Uam>U1nm-S$ke=)hj-_lcmHGbzI$&Fr*_fd?R0nv1jj*eCl|R& z6Rgs3WzvvI+;Ch*X{9*#GfjYtFBPIdg5XaK=kc&WZ3q?YF9;0w4;G90f#8XDvO*mR z9(Y6eL6b#%GQgkW1tLavae!DJEQ$=CDxWx+m^?|Y2;5jSLh^HQ~I8f#Lix zKHDB{0-j4JQ3wOaFl01593B97HmW37M}}%uTn!};1?^)J&K({m8%#waAHiK16{XP! zV=tai%-{a`(~ajkj&eBiP}d@ z^gA(A`CB^q&8*nE+}l>bz3X&o-Rh{sKAJ(&Cs$)OWUf82FKQ5H;^Z*soYS;nF1MZB z%A0PD^R`W!rS)}Y`8PZFnocWO@G$bPA`IPDruLbHAc78z_dC&OWu zZ7pcGHiNg?3{dEKk+v9sdka%=#{p0ia=Ah#67hJr%wL3&Vu3;=^B0KaLaA5=aGuBW z7m7(-#>X&`LP&}gLY`QPDZpP8A`0UDUYPp@0CxuyaDQ*Hu&>+Roxq-!rr^#?g;)%> z#j1o8P^UpnSqt?Bok-Z(g5%ch;<dfezg64eCO(qQhjcAI$QM!qibwG=5heGB$dKGA(xWt}tqDfG zuPy!i@u!vG`LmrVM3DeN>)9rnXLyLyd)rsR~T`tZ=_U!L6HlXc7`*Ep`%?tWo zpARs~u^xYx#{Voa1Gr)He|6*kLXyC(`NmM)2OD`M*FTAt@xqb%R%Yy8gRSsQ&oBz4hM$YyWQ) zvd;hgu&Ms~x5VsUv-M931=aR{!S(e|l00kuHwM|QDgWx0UDfH5nbNWJrTmVZaU?!}MeIul98} zqjTS!hMsbjGGx34+>2_tx}R@I%zvth`B$#+(q8~11f>k86x_-W>Nk-XU22N@SL(Ji zMlM*T2n(WIbmj1B-)y0c;7+8J0TZ==|CPRBmSc}Si@~Zl`~_Uj;p#_`ZBCbr^+T!TcuY^hlZKo zWrv}Jt6fMZxuy-qHeXMm4M1jrNL$RVdgBBB^#o;v?agLLNZ7VANJuZ(#t;ys{<3g{ zBqW%LWu2ezy7K?6GmsAw^4W3U=&SZ@MfZ4DIG+ljD@<^kP2|FP)bUt97O4F6M4 zhy=E+tQ|{fDTABUXd_5(m}P7k4R_+pWpwz*6ArMAA=MG4E~bu<>HS~*_y2Ei{~w`s z|A$5)>-gV?P4(CR+uQ#~U|s*;C}b}FZ)^V_YyWQ?dX4>maDwC$qIYNf4`?LfiO72P z|G~jovO(j21kMwRg=WV808@3bRpWnRu2{&!-?#BUxrRGM8f3|Bz;!kL>ifUz>Yv2H z!8-oe2vleM!GBB1(D(m3$iP4wIp*R zgwQ!O=Ke*q!5I`bE(JZYECHNxU^ju=-(05hVq~yk)f{^(PWF7;KVmQra z03TR4QhA#jsl>&NZCfKfOQLSvd0Bc=;VLZ^r1ZK1xPPnSezxHcz?}sl2OxMKM5IA* z3Z&bEf@86qbTW!6iaC7=B3^2=yPInX8S%N(9knR2dET-Fl8nP2}AoWUEV;$9APFA1OU_b8`NHKH>=gr&a7Za!wh&*On%J*x~`m2zi3 z9dU%)0yiMl-5^&52P?H>nyl$r!x6Kh{sjCcj-bC0N9fm_*X4-UU5TEbU6B9`>sDo0 z;=1J3bfgX33OJ%gl_Na#UWjd1a0D>7u3|1d_2-Xtgvp_^pmjF}!8JU(Ew|v5!K3tQ z9vwA$FWy9#xc%yX5tYS{@EPr9^QAgQ+@W)l z&5g|BuUi**D9Un%Un$;PApTG2%AQMjs&PA3OB1eD%yUi|_-(iU<@AkaFYZ!g^V7%A zey7ps%d4z8dM4=*9f@j!%x(f#FrCVnzz5b%Uf#j2Fg}1yUc`!9`85hNDk=H;%L-GE zB+jla`IQaSz#K1i(FCK8$ zLK21vl2~ZHr%~#XznA^MkhrZ9{@we0V)ieAPyU>->%|cMbosmok$$;R!5U~e>ck5u+l5VKJX1gvgyzWZw zC*kR*^@(5yyI~o3%%wxzD#YDLYC2luq-le_QIq!iA4~~sa zzS+A6nR2V!|Jl11XsFh=j|L@$QHW?ah~zRZ!=OTjBcyUmxr|}P%rFNIYE>cQ2(G4dRiKKGxDiTRW;=lKdI%d#5XZ_E2*0;Xz?6s`Lw)gY?e$Vqh&-1?T z-p{+~ReA=OP}NbDtEqKSFAl9M?+>5jA|C8_A9e6t*-*W#wh>$HjwzgzcC8I_KmoJOK!fH!yd;ad70ys zSxnAI%;aC(opFjS95Y2_5vae-4|RUPYKtLF6f+~gH72pmOqBte{Ormp=MZ|F*!-|N zIpj9If!4uVb^pxFL)lF#vM0}5>biDKDLm_=a8P99)$UTQQ(n?>GVM}?O(%aJnPBi6 zD>`^)ujrU)_ak+5Ffy-~GbB!2TE$DdNA~HA75OBW+~L#&7NT znWSt{UiLtY$sYbU9*2xf*+yd8o8|&hUiv_oO2|(W(CGP=2Mv6UijS8+{wE&Y{4K}W z=YQZYG-@CLdLi_0Dgos^|NU#{TDNnf1m&UBgen> zpZ~uPSpVn0qw!yrfd16~4UN&5G2eeNGyHe|r+?;1zB1puU+qWzCl34()nyxmSU@j- z)7b_LSBKm@lnPb>hmyoNaclOglT^YbWEU394k$^ryZHPCAR;EFK;UvH2FnN$%Sd#l zEd{(YkV9gz#Zg8WQ042eF&X3*==_HB-fmuOf zK}8Y>&{9Gg{?x#=+f?AE|6;j})g)r{A#Qb`)LWUld%r7n(cE_H7el_H$SWJgk%9as&+m5J7ywe^OCGXxEKD685OJ3_e39Sxa z&|njZ1K^kea9{rnf*y;V>5QT@u9n{u_IeNwL#C`CSiz$QW2i1%*r z3f`Ss(I`400v`~`E5SnOw3@#nd->ZyY<_LvN-5>uYa#_3~4D>%UBkZWD=Ez0g z`;sBr1dA~<74AEM{h|uh%D2m3QYL5bt6S$_z*(%eu=v5DY&rR2HbQB@obSqfjGhD< zl?ate@j_z+Sg9;zjUibhc+A}`&be1+P{$~xlt?5s-m_2+0~|OpIQAsc+-MYNAtoKFW95~a>V7Xn<=aJAwGNqY zTAz5iFkN&9AwIHlTp`hw>NASu9dG zfkjk6q4m**XrV(W9KTE7h*QeT(TY6n^(Ot)94(a=zxpT*jr|=RuRQi>dYF+w-NH2l zz$pcQqa%o^sADkbre=MGkMwK(Cxcr1wpF%Sb8~M`U3+ogr>F0)MtFg-U>qF-aQy^; zBP$3)ylFx4owOKd#pKraVGDHLZ-0s?p&Qsb*04)rHY-OtSb{bH1XaUiaM|M+!34$# zZt>DOS)Efe_p_^2XEo9p8VawTMW9`8S>(@FPDA^XO`rjvKugdXB$q+uL~;lbye5ob zn&^39YoR&~X=JLBW9jnSLjSOsW6f?Z*Ke0F^Sc(#Y>MQK04Hn$fYSv4M?nxxh$4Qd z#=ev`e27%EswV%#0-Y8WN=E&Rhlpyq&?mB~^T((*oXVjPS%h!`k;o!VI2xEc%WKvt z7mHVgwCAg|lfO6%?%rFBY3A7QF!@tiD446~~e&igl zeEp}+%2V%pUw(`nxeFf(4#b$maYCt(XD8$%mK`e1c%ZvMO03*z!SBBAjc2#(A2jiN zeSTS~<v2dUb7< zd~P{}O}2K=uAQw9J1+F91cB)n z*5LrS=mEfS7X+mjUt8Jpy6bao*1hbkUkz>ao<~_<)RD~9ck3sKpTK7NOa>-O!~ooQ z0)S&I2&H^=v)-bFZC+OH*iNE6tFAe{=g1So`__eLPWY4`C#J=SLUSCb?{Ek-S|tBA zS~O}cm&xKny&0`IXrsht_d_4fN2k?$f*v)d830b1033Qj5H-`X2Cs#d*wPnS z?>JhOwWZoee_!hSZF8rp23PN!xo{Yo)*!(s!{HC2+2F8oHHpBQf-*HWG{hJQhwCzz z7hiTRa7tPhSsz&2ey};pU%Mqa*CNa_>dcT~fG%1GvNTWJFH8b$36nu2@luVc8B87s zn_RVI`X-U-*E&~uTKt_dt%=k_0 zXUSGUmIb?%!4{_TS^(*Ba8SOM97)$gvy?u)&kB-F-S{$yo_gRkn-d4HijQAuaGhB8-;SxT0<-Mc zZV$jYEdWPS5XCY^Fn(^zE!X1@S~2Z11K*Nm5G5D~qp$wEUIr0jYwFRUc~#jo0-FMc zYy%uKoWY^eNsa_64L34tosKe{xc7)AH(KFaUrPDh+1lV2S5K0tQ+c%SKx^?S?KNxL zLY`kUg_an9hzAYtMkO;yL@?5F50N^q?lTtmol0jmKUs zeQj}+kdW@rH}<;|z@GK9MgTY^0B|$}@o4Ut&Dgoa`iB0V1La@NnQeIV@+S9sdjC$t%nIZ?DK}oo%SDRfpJ!07qL8PrKUb2Q4w_1>H$Hy21NTsTDdZS@yMFJvXE#e#Z62 zS0iZ2MQtJ_@qOq|Eel9geXh$$b3k57@%^$ffS~cEDqz~a+}nY2ABgv@n&;)KLJ16t zwoo(BDB&jocxl^)<^+7eD$M0)r%TtTJglk7>6}s1Mc6ZC%Go5<>05XiYm6)ajw%3$ zQ4qy@RP43lujZlg&$X?aY=2D|K1SMF!^L~=pPgT3keaxT_fX|K3I{5*xj!|KK!f&I zppw|)JcS8L%(Xt$kcKFqzH`NaVwQ#V9BoVLjbpDV!^a~_tLV4t>Xe`%-*<^%2pth6 zDzwbt6qSBgR>`GpQ~msuhS$lCu^;n}<9->~&>4}+$n@PJ;RUK^95DbKApj1aAdHIr z%o~7GS4Z+HoK#tES)G>|MXb0%R`uGtQ~Qmgm#aV)pE(M~k99US^xn6iEEBwwe{%KJ zB3JH?6Hgv>bFv4jOjMmh9}_%k$c=s*GIU4F@?e0oI3oBPv|R-fWr#I2F`2kydQ%P- zbuE%DU-t$-*%{Td!Tq)sbMc3=Qo|(~I*uEf-+F`E8ecR3#}I&nEr?`Jr5Y|*+pXM# zQJQi$eVTH>&5cQmOW}-s)5)em&v)% zJ&C~Y69SB3nDVj!ImP8T{o65thgqtpdqGcGW2fYOnnE^b&*=f zz5$enG;YjH9)Mel0N{9v&KQM(HS#!H6o3QcvxQQrAtGw`{RZe~CM1a)N@fv=q(4N7 z$Uf9*+$ADvKXO70I*EoZ4J3pT0;n`9ClVa{t)y{*X^@q*%-shq zzRVO|{x)n$be!E*wU{h-sT~=~%$nrH4N^Zg?}Xa?qeO?d>!3|wAa8diMFwHTt&T9YUYA(%)ZU?aK4CiEaQiJ~+Ki45S)(D58*2#LYQhccNo zWr+YTi$P*3OOCkwC=v_tzSC}dsYkDA)Njvj+Rpvld4U_WI`2A8e4US+8rmm=Phz#) zCEVj8l)d}VwEGHIuIXu788pX={wWqdqgZev$#k}H1ckyjjtn!wnuSs#jEUhU1anL< z!;E7_3#FTwp;<}@qymG-u6PSm9wremOsoAwEE#az7@zCxq% zZ42D(KbPqriQf5m_WP6;t3@Az{**qgN9hwyM4Lq#nPSLvIya1C5`_*oCxm0M=wJq$ zWrjuvn?^CrLl{cp$Vp)2_^!NBVD-#uGYvc)SLfiXZhl=<8pi3} zA#CDZAO6&4l~8DjmfOFL8f7*!8BuHs0qh3 z(41q=3NbeiBx13&NNx~KNen4HdPR2>+TzompQdgtOCGkVib%UN-aBHZ5#Md6MEQxsmt{FJ|)06c~qmtQkB}pgyDA* zyfi>7HGhfQr|AV<}cE z)y~-E!+W-RMFyQitS>ytCjWKY(Typ_=x|yXI|xgovA7YOQ0{-&yY_G>x9>mi7=v+% zA-TmXQORY7aT(o2E~lf?O^MRPAjWkp zZ*KBtUiQ?tKYo84f7J7M_Fn6=*1PuJYp?ZrU*2xP%utfIkAZiPkEw^DsjF##0-lIt z*9nyH?0~k(v7IfYa>MuYIL!yu6S)D3W+M!GnlQ+L2pG{xJ!7|2cc|*9yiZ)1&M-@8Jb6a9 z25N%;_&7=BXOFpDMBf9VJoT{oF+t_tISr3Na8XAKKV1AI#8hRjBR0JW#;gelq zs$FND%d!nhJr1#x&g+@06j#pKAYpT>IsKc(3l41sMG6e^77W6N03k8^(Ej=pJ26L3 z$Gtq)e%!}<~i|fBx?R@uk$NH$p_McOtQjV~x4-kezAo5Dc1|%b@ zF_l6!HZmcq^8XH5GO>+9MHr-PRRUGo-K#YYxW^hNKvWfU&1huGtxB9 zgJP%hzN_!chAXaldz$9D<8dTq%d9Et_J?#QugK~fOHKVNC3c+@+cKe{oL6RW<`7tr z2rXH6-B%E_iZ_A%?FC4WE#~{}#<$R}{`DVqdvKJ$@ETVYx|G#rjtuOtxRX@4T&!D-ILX2H~d`-PQgThQG#tgbo zFpV1I#-uWXC-7mAYH1CY9Q#PLUPooC+Yu|1%q=Ir9#b`T$~cl66d?j2Qng%Qb(I(JpEUb;{}CaLjSp z$D~S(6c&pnEseIHRg)ubg>>!{h6{ZI2Gj+$*wQ?KMdpvh(%jD^E zD(Rz0he5`|ApAdg?rc{9bPDS>FFNmej%H$p{hSWt*BPWepOobduRuEEwYj#UT`o9i z2!pmlKNx9hLTHPO_fJaBskEf}R_r@Fqp`j-ZQF$NFKqBQSl5Ur7Hy**O{}-pv9W+vc%Kkx#6{80|!fR9Ny~ZCWIs6^I z`AdSmkkgcl)3Q!hN%g5aO(@pmYCt=7Ig+IYH3;(sc~)D(Mr*55rqq?g%*0?bwc2jy z`;Fh~6TK`Awc2Df^LefuQ8Wy)9tMF&qFFT0oUw=MHe2lJtRovA-X5@YcWo{wh|i-h zIenxCA}r+e9!7B`42ldGWG@m;D#4)t+3e-JFzOZMmAX0VE3>0A`w22nZEZSJ-fy&o zX?TfI7X=Jj<1olx?&-}zGqg#9U^(9C3kws2e0dY3*pqZQ8!oRN>f3N3Q=L>Pn?1|b-xcKko%7fG}lyt&^khpXO3lCawCIPu!YCj8m?bxxZJ zy?OhvZ1dGo&Hn-+@&P}8dLZyN#tfnb)BK6Q;|zy|w7(xE212G*;HgH-bpy=W{5@@QouPzeTzNA`f$<+lPiYyYY0bkcfkSOv;OvR3ervbX3mv zKh$vCO?|(6kUraw6Hs#qROb#8uOKr=1poMiSD$RR=$-Ug?Kigg2k%Wjutn~2*@I25 z9)07aDCpv0&@+ZX*oR?pSaDt|#+=+?V;yBbX5P|U>1|a86E8H4`)pXJZnmXa&6%|^ zjSw<0D9mAyp~J9*&z<(~w<~>Bo71JfCN*T;RFZdIbTi%F^57G@e>PVTl}y+TQ4QYe zC&(##mJ-X>>daf~6RrM<>94)-e1y0l?ti!YsJ&M4RY`pf)R7ql*KX2^=wG<~Toa3kq~n_6-nu z?^~G@@2^_{kMZ60+5y@eDr+pN|zn;OSPBM%SGih zrmntXWY84yXkV1`xl$Kf))vQ#KNz$YVNfIw(;NJL=es9&O-ReTm&c*6! zvtyiQUXF3YOM&({7_?`@ATMw`euJ2X5-2EX5H8D^)U-`tHPWK3;AxkkBC*55n zA|Bd^Ox6t8V}Brh`x?%6``4u;$rF=W_H98|kht{LBe}O|y6yvB*DRw%&&|VgDnzJL zl_yzf+^jh@_O!x<`eig)-`0fE{KZksqOA(k4l%nS9ToOeNS=VYT+sv;m@p}|P5Z~( z4gR94=HGno$(GiPMTWvI8iWH*W3(w*oU{#sL6Bh(Ff`rkI)wI6^~!VGpDPKkJ*9JD zpRnw2rxw#>Fh}n9r#bKKjc;4BgX@ zGI#IMTfm&pKI3qLF9cOlh&bB$g91TwN}q$yo|;eMS$8BRCOO$vJFn77e;e-N=6w88 zMTtd00Lz^}0|5s42nHFAhO*(o{JhY6>b}R)?+3g|FOMPZ_6`Y3%Dc5b$m*}F1)^b` z1PI;nFeqSPkPFZNF&~TGxxI1ztmv)ED`#9!+7Yq$sO#4f8eD1!#?C8j5{;3M1 zCpv?*N@*;bWT@HII(K7IN2RuU#M-=IPoaME3xCE&J7BRd{l@2!z3H7VXSMBQiHd6PN+||o8XRu4 zu>LLmb$m1D{1`ntU{J`zAgduIK43$K2neB{SY~Ju7TU?1AjF;|#MCFp3$wNuJp!If zY2kiF3&dt2>JvnG#1-xYQ0_)FAP|O_C)_ze3X3j5n8Pm-P{mY&5Y5$WodC)= z*nkF83jyt=5PskwaA$!+l1L;-`Otk_5Rrj_M49>w8v9m+6@?%vroMk*kT0G4SFM>C z5rzIs<`^genF;Z=c(6c^FFIp^XC+jL;Dic`Y0FqVAVYw5@qRIGgoJAA(VBxAj?r)Y zt`(3yrSEq8DyZAXnoO@Q&Z%vV)vLazg~8$=3`|fRES-)uH83uz@6}IS~Nx1qe$JV;9vz;TpnK^ZCWLP~V3J(0xmTleUSeZlQi$6V%d59p ziO)z1VUUAhkTqz4-07`_@81;_9a{Hx=ax#%Hq+V{I?jS4i8OiJ5 z#0O+88lbK#$4C{)E`M6rt4ORCi#?PPQE^JCEPd(am+!k|Y2*-A;)4SvKA_l!K~_T+ ze8Bt=dpGBQ(CrU=JrqC*=wboBc?h5cm{EfU2%-d(m4nlE0hE9e0APk7h!Ws!5Hvs# zCBQo{Xn-I}K>0uTf*^nr(Cr6&JrY0(@TvnEAczuRK?pQpl=~DG25o#LW>M#quuwK{ z0{i37!#Odj-XfjnGtSP@4-Z)o`ZoD%>GTG=%;zw#kB6#-1C}c?m$HvDbefRauhE1Q zAbz4y0wr6dHgbMd#p|h&c}^I|!^=zuJh`n|+$`5v`5j)tquGf-O?oWbSZL6Q=fn6Ve1`thwTD_nf~RVA`q82?$w6&)PyBUtKgeHus^&7|c|k1C z&=Ez0sX8F=NHk^m1OBON->Ek3HFtY6DP4N|l9ZM|^%bppLMLNex{h_ySXuk1j1JIq zgF*Hp(M(<5TwSla?^5QKzqF?iGTv*jdam~1slcn}ld=0yF7@xz$9XQkk zdC^!|_o#$4P+-C!dm*#$X!!P2APosLU^L-C8X{=GXu?7KC@_I)6yZRw7-+y~!a)Q= zFvD;Z;UE$lnB_K#a1dV)Oz0a$IH>jsERLcu;;=Ai`K3n2#Vo`_#k>h(7(PavSI)}B z4hv%4M#jU;)0=knZQCEuTi+)^tg2 z`Vvi{_e86^reZLXtGjQx;iYl^Gm!;k95e%oiEmQ|adW`rozdlk`d7dP7eGFU$McIs zA1n-dc(~K8%Pld`9RwiM?+svq9ojC>KQ2wgKov+ysNWmFVv67Ew%A~xyU3vO6FvgvV5_>@jTk<7lo{RId zk7SKF@#C4dOLNN9XWA0YINi3P7u?v_NJCR9)sRg2{(>9D(A0!%L?)4q4Jalg6AFcF zLZTR%a}J*ngCkBKM*OfKTD(##COAq6j%X>6yKQ<*q>rXuttMIa~Ly15Cagb4grQE z=>S+hIsiuTy*z8PspZ>jkaPecUX-kyG331@NXLj z*~S)JcJ!Q!x16y!{79u-kOjY5%Eb=;4_bP_wFy!HEJlL{j8MuIf>?|E-&2}p(I%!gIy<(>KI0ZK@V<^*b9?D78FX;)Oyx4j8cZ^$wxllEP|5H=Sucg& zhh3M}n>cgoO*4z7tL;aiS$;*~w^Z&A zhe>lPwl~3^HM=a)SX*P{f3rR-*q|JAAAa{m!Hc1_}iYsmF z7%BT}n)Cw}QJBa#V-58i0>#Vejc{19tZ{7dI8XNtFEWIU+Fkt9ltP2&oqVv)O)F$o znwj#qW*=t5>rbR^}>d7axwLpC!ey7N5$Ua>aJqL+ZGl?&s;>)C$O;)$FCw za$0y)PD@|vEN9g`pT*1Vq0GDf#?|Y49Up8?d|YI~*K&bD9kUtHY`Hx(#K-cvDbRX3h$ga6gdJp&go4-U7US&>WIY!29$ti^9%BM z<~tl#$lfgG+gQltTrQp;S=%ltUjL;Fk$Mhz)Lbp(he{$}S#N zr4=t`SMFW(EfX)~7;(IO~UAb*9u^U`s4WTc*f%7&S#Cw#RK7iw#AcHxnustdiGNo$&%4s1_rvYpY| zelN0iUatH0ihn8-KWo{d!?ZDPtJhi78EYq&F(xDsX<3!$-nMS3*MDwv@%4Y1_!ct>&?Sc+tFwu@c6QZBpQbuc8 z{l@yy@!rw(TH+;VtV+{jZ%W>2H)1`!UGH3wWsI&;1gZc)0raywR&Vm!i=!8BFg~lX zBDK6&rsaaL+3itCf$H@x*R35Z?ir&iX@M#LPyqex#uaKl3lV?r*SWqOvmo>LOacz? zbLGwEsMrHHnwxLr-Eky|5`Ve^00qZAgWmV7h4N%zA`Q?F6)1*S!RXTu6=mS64c7_) z9)UXVqx02g3K;CCzut%#p3D?HrfGe`zT+g^=iYzd#Y~wrLs4#vg^~NPO*YptTW=d) zPYyQp)hYmVK#RXm$#IYN9VtC^pCbBt!D2Y^^JEH@t|<_=VozI=-7u<(ZCGku^qT0= zqvdMc>OcBkx{_R{lkP5AJW;ww_h6_glPxcvsa>Lzbc#MWO8Y* z-@M#Wtn)_bLj;ROpqLQIF>baGH6r(Ry=3ZCJU#bhg}7s0$wh;eLS07&} zX!qp!U}_3OosK@q1!DGWjxVshX*-Ub?^{?$m_l&Jm+e?-Rsr#m(6T$u4zi}huSl>uzVK?Tdy?PDZ$dO8ZGrqF3LCX>PFKhU{T zQ&g<-0z3D{dL8hIz2W<5&+eVw7VUvoH=mWZP@V1?5I~&OV-u#Pa337p3|_t!ck=$e zCn;Bly&_A5H(sC8Es?%pKN5X9O~EB;-)zj+TrUr53e-Rd6b(0r?khHQW;(T2`emyf z{FH6w(NQ-{GyS!jcD=q>h=1(l1GeaUcF5EeC|Yg~Q3?+xc9Ju?C74>j<;yxx_ph71 zGEpUa@4V-QUJ-A_M>`Tn5g}>{G`0~aYCb^qzPAa;&w>O5F#>UDeuiYzX9T3aK>~sp zfoMuFk4XR{AbSfE5X1;n(*%p2f*1kWRgi!nMnEnoAV9l!-P#jNC>O*QL@R}Prvey( zD3LH{QUD_m0}!SN@@A;u5$O51^RwA{0}OV2@57EKngkD@USV9^=!BaEP zGst1JH^tlC!O@=@XS=PMno*kt!W;yzD=W!MU6v1H5g1E1WRhbr~Ay0jzC{fpzgFSstq?UDXv*MP# zM8mBtwHwRPZ)zSV1wauD=A?i_OJdzKkC|_>oMX4x-Ak!#dajUqmwf%6x}mck;KLwz>xPaI zfX^A=ts6Qi0X|BBw{8gO1wz62=mrlxfCTvH29JD!1o-HN&SDtCVGVc$nr|f4SP>yC zb~^U2#mbLZ*7v$hku~`(jRZ}SoNr@sAksm7mZq9^K;*^tUUk| zbxoQkQ-j=xWo2lPX$pmD*n_N`4JXdj6E;(Fthj}VP?_0pf3Qfm|0GUDt@pKRY6?s4^hPrZm%yvO!OgIQDJEMaUfbzX=s4< zL$nIu3@1btKLDb7`hA|is4$k&JBWED>HSVola9Rgi;DLTjT$~zwqfTVdWo8^TG z5!)smP`S9|nNLM{;X<+}c}BvUd(-re=6jjg5=gw-1%dh#ftrAuL*A$EDpk43Tc?a= zGxJ4{R6BPp+K*iod0}p`_vP7}EmL%QItotGYfu;r28G;zpmR^sw<)Ykxm!n%fAWXd zrY`y%7x!WLD;7Rde6>f^H0gHyQ;%6aHsK^a_rbxEetJlBr0b%~b@1MZ?V@n`*z*k1fpH2IrTt_kI4D`rcAD$*kdM zmEoPvRQ<(zD{ToxUabx%>7i)3Zt2xh^y!Qx)71(SD>i2^DeDbf zN^Ut4N0Fc;Jv6ovC~7{rI{aUMp+Xc~sS>~lSf7Ig1Tg}YEPke@(PsoI*uWJX0gQlk zEJ#2QBhVrWTxAJj1gtYb0)iL;i>;7=AV$E_J0u{85vY6**Y^c50u@2wDk<;mJOP0o ze1QcgCrje6CcRHNZloi(_Yo7Xy|PXe{(L#&b0urZv&AWX7ephMo~=^)SULAkQR|-9 z+22b#!Ve@Jb;K94tF5Gz6c2aIbEr6!e?`>vQiw;ou1Ij(L-o44map$P9;72Ef}xU* z`uY(1kORN;AOj$63KHN!IwBxYyBzeZOM^7RricE_7w1p^IIMEIFupf39dS9|O-*gmOLlCtSN_)QgXIQk=XT{ST2OPT#ZUd^(PKVkyHxMi zUQ^z5|5nCr+WB)YYJ1fVmbqh;-kGdk7@bHlJ*@2~7o02Bw6yZk_ANRA4{>|Kf;VaQ z_T*16cOb_(y^-mv`p}9`I8fjG;;xsrFDZd6hkv~4{+#FZ{9|wN27mTWAl}R!sPPdf zUQTa(Ptkv6{~|j=e}eheICALTt+#{BqV<&IWt_!HB|FYiS)?(1dlG>LCIZFFg%{BG zgopTQFrkf)a)_D-bNBcthqzoY*^7^IDCr4iMfoWQsaTKzKjk1X6cXU49Hg{C0{oOi zbU2tV$45Cx+=K-9DF>;+kN`jB5HAlV?hRz_5D@68NuDO)Z% z&yLm_5T|?I+1$)H*)6bL-J)sJ%Ba(^W4bnVpE5?D`TrYx4@C!x9e;JR%-Q_)qa8)t zuUf9JWeDj`+Ov80R7`4-g6h(JLg+gTXF-b*65i2|F+`9ZLE7<^F;o$sF}jB^Bq^p+ znG;6N4>s|PYEK9{R1>BXsF#I__C35dUPSM5%s0Z&v-K^fjs~voUghT;_eT8uJ6vf2 z-G;_2J3LDsY(;I;T=w~-6>l7ig4e?9Bk7DmiZ1jrFvz=x6`x&7vTCTz~L`&+8i6cn@5r&`u z`kzXfF;YA&a8aws$rHM5x1R5gkwn(7%d?(s^SG-cci&_`N78uGPZNfq;J6cR`X2ER z-)M-0q(1!+#|kE4@z)P=$6#_9fBjHJ87w(-CnOONXmCU|Yu#7CV7&$u><18%2$%`8 zLt^AHSZ@#}O2EX~6*(GVus(voLaX43n5(zV6{7Ol#{9u7VFCctC15I z80lhjE6w|s&&uM<`PvGRUSpNgl+U{_OF8ON^M07t*D~#QtVYiVzlH`=lR?q!!)h?q zX%uw^m8wD3)TGd847xgnqCw-f?^s{&Lq%(_s?7zfK|r8j81r*Y(5?@Fu^I%7cVw}O zEC#y{0O}Gj_xHuf>te8hoPq-|H3YDz|B4j)3{?{}Kn>^4PXrJd66vSK%)?>V_k)BVE|3d?D45n2V4E^j3zYlgz@MPf&`R}3{0VvkG^zXHPY41aE`C6P2y#Pz6bL~8 z6bL6Yg&+VlF>s(#1QG8;r4R%_rSt)q2rz(&6JkObxC`39;PPR! zebrRaRVB;IOU;qx%Thysc(^*SdS;fYqrKXEJ#!Poxt3NlzAYVxVx#9i291X4U48cd zi~c7AL#C-ydUk`<(4b<}$r^Mz9fJ(X*kCU|bb|UI$k&G8CkOrizx2vcw=zWE4_HYL zCx0goM|)NMKwqZ-*R@UtY|ox4@5}bVVenW2CTVvGiHO1Cuy`Ed|Jv#QuD|U9=fBwk z1Jj?h0nUH)_mA_RL8USNoB#jBB|!fh{j3eB|LHV(|Nf`^_x%57F6|#LWIoy$bQOf6 zP9Y=HtgQ@`kbV-vWU>}TU5lcj%3x9%sFVlWMc2d0#c7QrVpx=I?&oRf>fwY~7G?MR zwR8<~Ld=V@=dSVf2}BHvvh`Vx-+t?V{jKl8c5p{bormu+!SY(`N69iZ zT@i-qV#kqAd>qL;EiSBQEWad6@*D1zNAm6{@kol~Njf{u+-*&Prmqx5uq{KnwkQg8 zShsb_*0dXt>=A4QwqV1u6+^e7ZPMhM?(f=LSSjsLbrlDc- z&!Uj@!v~cztQhw{|Bu_{*QLI4L)(4(DnbV7*{EpW`c%=8KRjK(~k#SyKHIj zoh9EqUw7b(5PUHTmg1LP9l;+yv3y2;xNv1kSuz!cPrL5ovM_52!&4_#*rg12Og=j~ zcQM6I8^??Mn36xLFG|Ddb5nE6$12tYKP;3_jE%dBJbBi)ChX;t=W}N+j1>+aW@1t~ zC`UacC5^IaFb#t6el=#VnhNi`C%5g&2YnM7(6R+>?oWc719|1 z2lO%^mj(DcBaKTkLtT6G^xx?Jfx|=Q-!vMiCg|uotpA`+;8G$F{x5DC2Q9`s-2YqX zfAq}E%=oFfjnimb|MT_w7e$Hf#{aDtb+>ok%B4zoyaA(y{}Y4Yl2G4okVb3&7i5-~ zSJ!`>D5tot|8K=G?)o41@AoC*opkVj3Yt0HGrTcd{iA!XGU2zZ4=oon#Gr}>SYdz_ z=i&~CgIY=C(ifS2MlC9al{KJxWWsk_;GhA$lcAW|sL*6MgtRSBx4g2K4W2D9Lsyv` z_JjDLq^xAa+K+*XjECw=f@|<_LBWFqhyq#48n&-w@p1-VbYQOaGttw82$53IvsSs3 zCDYCMzl(>NC!^wDsu#9 z3n$yHtAf+iG*|bK_ZwPZ1+EMw%M4j%(~KEFedcr!JwrIx7t4Xd;p;UNIe9!3iPUzD zE1`NX;Yoagw_40#h+0;+l$@z+2-MGLdR{4;D1_QGzFXD_0#gBUffUw1HPtq(00)?> zT>=c`wOSNciO4Oo)W{ZLYQUOTb{w;EW7qWupkPbBf?+$tXK zdtlJ6zO1-f0NwCn6GGb-MwDjY4ZkV|@MXgSuB)dT@=WP;iQA z+RKrfAOIv1umU1X_?Y!q0^85o8nM0-I1$9+=Ak8gj2=gqlU`xe)j>XI<0y>t^1xD^ z$kIx-Wh@Y(<6Co>hJan4*gC3VSk-AZRP2Y`#D+oAKnY1VPOy!h0b?R*K;&EHng=uzh^4Pd7%sS_$gfsK!PEVztZ9TTzwN>0`Di%$F9WT;a~tW!o*6NQe=H`ZS2vT?DBYZ8J!eVzY3yr2BSu zHCz)^*Me~6hhiYw#kwMIMqvx$rO*VuVp(O7KFD^-2vZ%#!$wZ7^)ss^?Pu%78&e~3 zHH@)qQ?7!S!YZU`xUK;+f_boV6R?5}vw#{z(*qev&47YFgw*F1T|ozs1dbE=Kz`Bm zf_gW4Kdr7_#ekuO4Gq*DmS!&p2E~N=A^<=V@!2&r3nwkoNX^3y?7sdY)pz~+k-PU$yP{rr z+y2BC|8#2jcN2+QYVq$TEjnO*dONja)h~CLp5ASTYJX3`zsYhOdXwoNP0XxA?B^4k zu^o_;xOn%sK409P*jcN*UL~g*GrH%XjRdCg;?CZ|-T`WVJ)13fy!%r-DUG@ndv@ri zRNYw8wceklc88zc-e~FAl28=bWpl~Pr%lr>ulP=RL0j@F>_UlOD3{U&A+4qt`x3o( zQ9FZDpqJuVF(Z@c(wCqvQ@fE0_PxCsYG;r~HxK<&32L_wrI(intPfFpU;>6?K-`N- z$SY2MNdbIHsgRNQis?!Dbn1$*pqpk|6(QcZi@pLRfIGZDvYFhCQM4^R|Q zl;_~QhNxZmVN?0+$P_vj(^d7ap37R_0RA6 z!M|kp-b(j&Lw^nZk()F1^mLMXZvWQ3`=0&ov3s7GI3<4aiS0jr=(YdsdG_1$k32Z> z+u}$e9!?v`9TEj>`z? zOh({R?NU!lNXuMOGvDz(jI2hHVQtpclQU$aw5-s>_`8b=oY3oWnsF#lfty! zHp@=p)tCSMdmrALxUF?zD!kf9y}qXx*ISO4G{F}oY9F}{&O-LN`oi2u9IrCV<9%D@$RZ5_Ct$HOud(^>HwE?Tk~#f{T{N`jw|Cf@Z)Y}bNls|ZA0w^#zrFUAANF)x^P93Y-_glpr0o_o zbrPR@@e3zDv@6lux;0OApBZ%v)3;M$Dx`Tf)1ok~S)Q?7VM`-QLmAiMvyV$Y%4CFr|#=}lRe-qOh%St9$Z}2A ze-)A$DJ|b@>%V#yNVRDD-li=8i9X)>`i}zf{}kI@|8XluEB!}n1#NUO&>Ppk44=FD z--^+x{x`B1XxsXauhTzdpYHm9TQj=rf4;Lvr~Xf@W1WkE-dO)f{6EEY_kY-u(W(A7 zax<{4{_FRD;yLIEyZYaX(Y^oo&K{lm|3+>G-f;ZqWGU5+|64LT*Z=x&2Da0Gf8O}fO#c=x2fLtGHn!_mb&&Yq zLjOFUs^9;Q6}tPsY{|IFkp14mg@&Ww_LBES$*$zna27*z_<~XOm$S3>%%HNkWG+1@ zE{@99)PnBKI_B)@nRA>f3hvlxr>riVQx}9qr+D0&5l$RC&!1ViBD(sNH8?X`wCqJy zUYu-d4wO&lp?FJD_t6tj@|0{>r*U0gw?tml@25Ror;EPtcteAO z1;a0vb4k@M4Wh*in2J>(pD4Ke)YRPg5It;FXh-pjbWS(zWm;7%_(Ur?&o;}xP9M~h zg(S@kE-CI{siG|Fo?X(TeuQ~|wq3yF>i8>xC7t$d+AjimfT1` z0mASA3-jKiQzypUidrzX$S9+d97Plvk(&~~2aWq4$ClCIf9xId5*p0X5>6M%vG>!CpCB?nAS13RN} zD-CkNT&yKJXIP3`p&Zv=8Z1mwEtVa6v23UdW`&rXy=WP1!uAAf^I=M>P3r47*qJG`k!KSHb3k2%})$p;sy z28R@_pu1WQ&>X6{>|l*=12xTe2dgIsATD@5?buG)ga~Ss98)jpmQVEg0EMPIKs#j! z9R)l52B9`^n?qH84wgOLqv4xTu<2zNRI9tu)i}-YqpO4Lz|QD`s)mQ)!YZ0v*#M{Y zl>#trhu;R^s~3YjKuL@Xdch7MTJXGvL?hH|Gg5>4P;5DktY`&LW|2sbz_eh6%s)cZLINSk1C*A_ zhE|TA8`~ta$`9hukKphHK}milce9UC@0c@%c5G(;Oq9M>sm^m<_ z8c0hqX)=*8C5H^qaWJ07lp3bPW3yAINYbXs1~!Hj1`RsgdX0{M$O6gRC&9&r(~#VC zU!hSf(XMYhj`8AOkvbrICA*Un<;#M`daZ;G7 zL&Z6~Q&m}X%b;FSt8OI_Zcgf0RvBK97lWxr+|^eFLy_%MTGjn-G!2- z-2^tkICTl^6_sX)G}1u)suM3>vm;K-R#(rnMn@y=j4A5>+k4XHw3RLA>*H6bNYz|>p26%(-el^5u^nvV4ZNmOl#m3- zfJBgFOj4PeA2Rdj=9kP_y0x*`dGXDCx5lcvH%96{y`HttInC;ZLbep;w@;sc17_u* zYy{$p4I2t?__Jpy`&>C#Ao*`eD1$o~q&$sxqH<(oMe6QrY`;k`wW)UO?A+$1;1}omkh_yQ#a5CzvE4r!V?AA^%rcO z!Mfcem0r@TMFHCap3vz{Lo6OMNmY-bCT6E^#7Z5@Fw{ zmSKBNm`|Xl-mneSSzIB&@b=#N*y)`A>y6QC(i+L1+T)vV~cL+3)9tw?J0AW zd6Y_X3fmk+eUQOqms}v_G#kDGJyAzMK0)dS7zhA5141vXDg2lzXKjTp1fvdTQ1Y|r zx1r;U_E2ix|E_%V=!24KTi>DFH<*i4Z_>1SwrA<*NOeQ6LHLTjKgHRatK%I7B+5Y8 zq4ODJQOH7@*}ISYy~oFu+%H}#DM!3 z+XHLI`L+>wBW&IS%PZwsCeRr{;F=92cPev(UB~O;C6ztHpWEMNs4C*(ts)FNB(Dnr z31hw|cqu5NZKzGOv4H-%TpQXPk3qDCZjq58O+yiVAimq>PU3K~&kP4eEYm<{H!vSz z{sjGw4hy=vVq}LzjDU2W*w7=!5~{t;II&^i04+N7cd^0$!$apA*u*G4C`BCnM_{jG zo5jVsYLZ3=19qSA1=<7&Hg-ezX6x1r_wY^a^0aw+_#QYo7E=YY7idHHlFc~4>c(T> zZVrkArX{gNNaX-#Ft&RzVMxZ6`3w+55jRopDD$DI4Ov`KL>LxcL5-^-#UQ-l6%}iRZqcT7z*l^! zHg7z9Hi2MP9|OWKLtyA`Zerl5pa{K3T~c^7W9FKDR5A?4OyJZYcaZ#{(XFzy<#5v5 zf`#MsweadslIKh!BEA8eT0(UU|0P2;!wU4g-yEsM`&NmxG&LHL{-Dr6clV)@P>p;N zS6RD@9f1@0k-RM&pI{1Y1m)O7O;40w}^_=>5HPxBd0F1jG@@U|B+6im#_v(kxZ zvr<;tyxjAp2rmQF!j67V_0LSX`F&a-y9!bEx*rG0%q`Kg8`xu zOJ1>|fV$LIz8W(c?$ox>v`MCRZDjZRk|S&{Z8g%6NSWg+m$7*v_@M2;ml%6S9}mn) zza!=&?zJZ*v#ppxF~NGEJDl0BJ2tvP)MBwJGg&QIsS+riQNSjfstY_!jLAp*u{&|_ z+Gr}!B?uo#<3YfeBv-g%p}4#w%~r^60*Jegfs2s9v~+Z{GK$REoD|$PLXwH%mZ8Cy z#}&|gM1x5MK?YB6$>t2}kb*r*2lIHErjfPYcnTp*8wn9!T~3X0cW6`MNdsul?sx$l zQ=N~QdNy4sbc7Y2mOPDAShZMZWLvm{2em>iV9W=h3E#{pW2aE^b<+_`jDmMq z6Q{<^{`%qI1{BsnP==9uc5yUeGPaJ0((Wd8Q_@jEakdESu=ySy$2yKVWjTdMMq}Mh zh|# z5?YwQrxQc8rVQ0l1UqtwdxVammwD&4$(CuOwy&+Yg8ufCWQC8_8Ylt6B2J z97ZWlFxOZHDXFC@_@p?Yk+vm)^F@JMCuC0{7Y4k*Iv^^rMck)&?Q}x82$2m_)ig}V z#{vr=QM6rl!eQe;wpEXQgf&c%>JGZ2!#dlCVynI*8)D8NTR|gJF#=$l6?nfgQL%Eq zT*y|KmbJw=h`y>Yp>!s#vy_9UHPQayF%JyDP-mb&&$emUq3;O-5ztF9c@3M;Xl6|8 zM?74mL$@ekfAfn@2$R@*-tOCyy4g(NfE1XhBp}o&VkV*&Q5RCpmf*7;LPP%o`i6oZ zqM=;2zUP8Kmrfx5r$qe}Od9;1;ts8_F$R1EO%I6bL4$#|Lc2nJ3YA8)2$mEk5u1$R zA)*S~<{K{>ui1CGhO2b;<<9g_ZCl#iFagMeSkn`Sc}?E{>HzD(4j4MC&<2PO^kEC# z^3ipR#DK_VAY19}?d^Nz%Azf!Gw1nHTS>88EwOb2JMND$^ExD{O()gpV6<8P`6-%2 z2ifRZ^cnXlwMc$OGj(d}=(%LuF370r)Dfqt#1+w>2{|76E1?fX-(Y=vhnOL7Mpwvo z;<$aH;%BsP$!H_S8fGNOQw&wdg_nQ?oatC4Zz!Ng2oYsy_MJ*Am(rbuTyG>GVJMON z5s?n<(g=;z;b?#S-k@DP7`w^Xslpxyg$!j?CNktf_D&h1V*?;7%k&Iz>jbDb1-E=O z$ae4_N=CRP(u5JM8>-L&70CeEYjhrq7#taS&^I;8P=^do8g)c}L0@AbDTLd>`ILih zC?GMUe2HP3U0w*3uz!-PnLOf`;1_h>9GxSk<1+&k6$7XX>3&oI{UQRV0e_=Rq5o;6 zHg;^nN{2^`XK->bxEh*069N?KZSRD=59byLKo9LW;`lUF-LbuyX7C;(DIbM$VU}gN zFUa01B~ekAg)($*-J@KwO1p8KLT4MDo%v9G!IDzc7X~)Z@qkM%*n1U4j@)iuPL^l& zxc6kCRkr>!2QADEW8`c{I|YRovO9g#QZ3CSpi|FcRcOl@tM~`6(!_Ln25c46o~09b zP$L(WgaPiQm`Efw;swrIs6e2xWJ{G{W)jm|C_{7@g#%2G>J0l4xs1h((ZFS7l?4f5 z1ivUiBUhiPvqi`Sm$haeuYBCshMqNoDHoj5k;ffpZ3R&dC7&&KF-3fbdD?X+Q=-^6 zh(yBpMZ{=FVLv-4gub47sA)V>MjuV4Bh0*ARuwT`E1kfG!ZOxR6KwPbT}7?;LI#aj zAlT@0$n+XFwBM$PaT6!%@5#|FyR9T2Gf7_}*;Z0)9Wn;0wjl3CLGU8JoK!c5b+bwG z8|<^50{i|k<2QtB+cR7evq-NeOGr>Rhjr1*cWk(*>*xl#UlEWU@@<;{-`Cj^764HG zRrv2RIN90<8p-SM)ji(>ZL?YNr z$P#lVcC$rp@~G~i$qdbWLv&C>nj;K8ER0RWJ{2>zqu|}4B$?UXV2GVj$B5($mWXhI zfxgQD1v4Z#3r$hTCm*fzwml*P9@#TVl_nFSf!(nKX^jtK1h(_06a#&*#nGVrjQj<; zgJoMOHWs=O;cco~rlxoxu@E;@$y-YZhGg{!8p6Y-xf&ca5#jr$+a5@4UHs9dE(g&;geU?Gn0*r?(%DWxFlg!-%8oqJ^ZUA7lhmn48{Syie zsB0r!ReM0zZxsb^m;2AaPlIa3ThuUQ|(UFhg^O|$=uJ|uR0-HL6hSQHZZoIVrrnr zQzQe)9yfHHDjE#A5J!wE@5E7UIP{0m34J|;c>F{tO_0(s0^Z0RN1-wT;ejyX+2vVlkt60%@J-4d_2|r9Kd-*N~)|ptjFEUu^unHk}!5M(jPq5`N(4pd%Wv z5$u#*r>h zuKcn6gDMj%|FJzpy(cd+8r>v8G` z1>wL8P9UK~GDvv6Z~_S~C$dN=$wkvX$W<4o3<uYp?2+#5$D$^8lDgr$))PR^tpHFcwZcvk6f?^; zU1%){zvf2I0ilq8IELn-XLFaFb~TsZ>d=-scJRmdon&w)AwtJe^{>O(A5{zYLsx{x!`6l7 zEDUOa&4kBA-IZlv4CqVWrRaSa2RYo&=qGP~n8~rJbvMr@?Ad9qSQ=&H^I6+1XNFq) zDCazl%rDj3YU%d+|%{^*owt9f|Z zZJjmtyN%}M{$2;>6!GPYhsI5s37q4;L3QK`;5-ae)D%JSfDc--t|IL3l~RrG5;|6QtB;^u1>Pkhr+$HxSQTT z-FWT!Q}g1wY@3DK#lhubc>YwW8?({fyzt~a-oVlwI=i*HcLCDT=kIS?+dt8>ivLIa zo?oGjXZSyUjNkv4$-mwI^h=&^iN7$_LeGFwIom6j3`5W7s#T++7t5J)sREkQUr0qn z1@Y78KaFbh_PpR(iT~5->m~pDm*?<5TLP?Ky8kKvmjC;gJil-Lzt?@%B*c?XC?}pg zblC8`d%f%^e*eVb7AOp4ga-i(J@7RnmiUOHeq~M?HoEtNY(8`jkEdB7A6%st{)suu z41h|ZffK{v6AIZZW!S@`(X~g0mHiv&L4cL? zBZp4c3^oO$Nr}-&H+0{#*-xnifS*XJxMik|G;~TEvKt2;B%&`i zbteR#z{Ft-AUle5WFbUxzslL54tqiZIPLD|cd5+>kKWx$H4 zi;kVHF<8(EBn>H@SkW~)LQf`^dl&l0406o z*r!M2YykXa*D4q`;<|7KnIh#Pk|^M*Gv4Y3dO8#EAmBQ!G=r#dB=G1>V@vcP0**g* z^b48z`PImY1jkPo>tKoq9;n#k>--UJc`BPB+&iMe3s z2cC_j#QlFuMmy+)_gK6eK5T-DPRn?7Z3~9x`{N+X0id3x3FhZ#lntD;w=mz1i`{`a z7}{ffTX>M~#D+l_<2x;fLly%6AND*EqYO5@=K9-KuHxp6K-?CHC%#Gfu1Xm`9S87) z7kd=N!w&xra~Cd40Y`+Tp_hKpkBpBbpP>Cm%J~vTFx>dC%MAV^qlrM~Q@+tVOrrHY z{S8oiX7)VSI5rmFF-AcwGl_>zc*W+pLqncCempy(o@hoA4BssU*M9#=MJF0`yZI!| z(rJePW5zD|2IkkCV(dclGkde71W=Lw?3IPPcN>Jyj)DHtbj8Vw^f)LXlyIqZp>PV`6^v9e>HxRlZO-dtu)sBsL5y!if(l?a&GJ z%%>8n%tjXehzIwxtcVbBkx*~Rmasdp`eQie3?V{!i2e~k7 z3iUCZYB8M%B`1mIUCHLNW$6$HG{p)B-Gm_resVIW7-fcw_{qVS6uwPGeb(@dOT^h- z8mcaQnj_Fd%n+mT(8pc+^pgZHg?EbUO5x{J5X+vG&~Gs9*9*W$(u1vlPv1igNcb>C zbz5K5DOz<6d^tNpUNk7B7qvk_uA&8;Dg2OHk=ToO-xZ}f+&jB$hc^)YUfO-imPm@o z3Bpa%^vJ^yb(fjxjJAE6pW>n^%AOkRrdP(LcN=m2(gwQds(+{mFylUKz<1fpAI1As zc)0@BTKs%p$y8XkKNZ2a5j+%EL*U;ewh~!qB}dALz^S~MP$ERj=|re7Gy=@zMNRne zz|X>$B>}kjQ$z%~lnQY*Et!e0#F(QgsQn-knI%Rw$0V}LI>H(DtluA_E)iNiIor`y z11$x4r7gaAPfBiw2Kv_orSWE1uflLIw8V$nfdpGTWsWq+I{$Utu;Md)w8$#^J z?w5S2nyLOo*y7sZzFWT>)X5v8_u}<<_q#1eYHsvsNiZ8IbZPy{V98a>A~R9Ypvd}7 zDnd|Xhm{1*dgm@%KTI~P0YCqNHRj&@=sIVA?K-c`4)IIZIg&FXh#_ZlcaTPj=1-=S zz|gx0RYtW8MU++f9;9gxb1sQA5#gJ3C=~#x9}AIfLYis>-Cs6Vp(RjctQiUu4ClZ| z`9akZNMNRxhIuF8S3wM#Z;lS~Lmwr7q1uYcpq zYqasq`G299$;HqA%bBmSe5(tET+rDNe-PIV`Hq?jI?7TF84legv$+$5ZV>(Sf5HpOpoAJE^#wYU$y7@s zenWg7sPr+F4PImCyQb}`T>?%%s7d%y67$$FC{~D zsZ2RniMSN6?7qlv743-c3lQ#!h^6VGvnhF6B_yX#3cuZ?c=I_5j8kGDsPBG&3}7?m zfWa;#{Wrvmf{8~T*hmke3*!$4jkoA4JySJ2?4VUC1`C37|AcYk(%V!?mX|($HM*M3 ziD+nYcy7YQ5CGqI$0g;UpE(;F<$J0%dTxtQLQo-p6NJ!G3B!IjR)!?*_!{-pF{Y2$utu*l*&4{3(MIA8iLR4# zyCS{Yp zMaG@*;?p5pK+@?Q$liu@42?jTnZQxPwPpI%F1f0QH_+O_#fIjib)+SbocTmj5W!7b z6+Hn!j&^~{sx%VN)s~_|!X3b*0Wty3tUEI>v9zeS94iv8XIseWR#q%*s))LeK2zru z4e}huz)W^Ab>_N3if^AjFVzepSNsM=^g{@Llb9vPv_k{&*$J<`=i-W@;w45Hs~?JO z(KHX#8O%M`4dQs#77z5j=Ku?jkJZJ3QOZ0vt40~R#SinYb@>!|c}uSnA42b9rD1I- zz#?24*NaqPpUy?=fE)(fIJ}(r3R9l0rwkmv6(e7vV6GUMTPAd?c;8FHy_Ug5;=fk3 zgZ+{*VdB2sWhh8z{5Y}DZi|%gW5?DNSdJ%KSJIC$!-Mbm*Y7Jv84g2RJxGoxQ`W?$ zt(+D=TuoUMuePpMB!q1ZU3$^5Rz|snNquC_mg3%ih9e88DG$WII9b6+KILaZ`Jw>haeZ;= z7D?=K1YbnPq(aZd`~^Ia1{sx~f>6LX8I1-8zJ!#9BL{zMuPuiYZrE)<6k_-?{11W< zSNB^6Ca(M{0ASaa;uTjEkMFwGU0S2rtO60=4{;e&y!J67zJZX_2H}#5A1@g%B*G_G zFq0shGR{c8uibg!kjvQS`uzehH)|wOdBlDU%ObuIB{NY915ijE&<*lKic46wYb=SD z3}Q)Qqxx|h5q(1rCBDa|D~7cjY2!0gF<7jy8cR2S$gYu>XkY zxacsm62=$Lo_P~y_xXHWorsuTq;o45E^ZPml8=UKcVjhHfM2PEQ97ouSOfeCd8)bckfeq7uPRoW^lG4WFB8efI*%pRM)TortW{bHs3+GQ(V0cQB8iW?z z=L|n|Po2!0I5#cbo(sGRcZnoRE{fd>%00Is@nDavx(^_sM{KVp#F(%}kQBH6jKib4 zq12UPikBMNSeu)DPM!{S95%E@xkOdLpQB}sPbgZ?&9v{f?wjbdm29C9n+2p&L84G7 z1znGQ2E_`!OyFBN^d+Qj2L)){QIm!qN1>&e7}MR5c&$ixYR}{LO8y+k`BY?`HojVy zU$zvSHnXRuTW@umg?3gm zPxg);w8`~QKfE3_A8fPrP+PRxnR#oGnV+_6Ppyu7uY`J={IPe}m(%wRx*i z8(d#sHkyZ-D*X0Z4ehDbxGS7?YEMmbZrH2-<=xW|c%`WyI%BB7QwB-l8Q+1EJwe;}om%E3r2mL|cYUb~6>*+x+Rmy&xnpe}= zmlG{BXpI;8dBJLwEBB{I3-hQnxH@c&9xETsMy_&sGOf<`bG^GNsHfA~Ve#~^q@Nfc zZ|^^zf63pU<=czScv!!^zdxzfI_FmYY*wuwbSB=@S*>zAXjFR_cSG;4HLZL-Kd!c% z)V*1I8k|>(7dKyK)$?4r^ks14>9@5nNA;slYT7u`Di_zer<$2R=zKjmG_3MaJ<-ud_`nqxYczb+#keZcmCm$c1hqa0MU{22->c``Pp^mbRdZAe;PICRb%H(>G z&UqgfR_DmPY0mqdtGns=w0Eoz{^;HK_WbH+J^k zw$kp=psltlrM&CyRUUGe6}5LbK7LRiAFr)NZd|RYt>e?hop!u;u)jAq%!1vYpMn_F zOg(s7Njtrm51#P2Zu&I3DRu{)$>p^MtYb2oem(FCR zo9n&fFO$Xf{$;J+aJ`9fmvW!(^7qwx!TmbAydPdW!*)g=I-R4d{-S@h$Tkb-8M`~t zuWwK0M@3g1?4O+s&E~`6I$vrWdhTPUd0VSw52sfHbN|fQKT~Vxj&|C)9ToNq?p5mX zx>GF~<%PblPl|)K{jpTAYDXF4%h6-AHhJg{hlZu+F3m~)vT{;e^bU^m-NuRC{g|8H z9387xbFbYm>}QY5x1+(xK5u)DTId{4S_}RDYI1u!xUJvJn;9r&+z;;6=H*>_ytrIc pQ_i%1RnHx|TH(0uPMQ~g^u~XC-k!JT?fHNH{2yOcrB(n=0sy~_8Lj{T literal 36798 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 Date: Fri, 5 Mar 2021 21:26:49 +0100 Subject: [PATCH 24/33] chore: address documentation changes --- server/rosetta/converter.go | 10 +++++----- server/rosetta/types.go | 24 ++++++++++++++++++------ 2 files changed, 23 insertions(+), 11 deletions(-) diff --git a/server/rosetta/converter.go b/server/rosetta/converter.go index 4d1a9c2f33c4..43c9460014fc 100644 --- a/server/rosetta/converter.go +++ b/server/rosetta/converter.go @@ -214,7 +214,7 @@ func (c converter) UnsignedTx(ops []*rosettatypes.Operation) (tx authsigning.Tx, } -// MetaToMsg unmarshals the rosetta metadata to the given sdk.Msg +// Msg unmarshals the rosetta metadata to the given sdk.Msg func (c converter) Msg(meta map[string]interface{}, msg sdk.Msg) error { metaBytes, err := json.Marshal(meta) if err != nil { @@ -237,7 +237,7 @@ func (c converter) Meta(msg sdk.Msg) (meta map[string]interface{}, err error) { return } -// MsgToOps will create an operation for each msg signer +// Ops will create an operation for each msg signer // with the message proto name as type, and the raw fields // as metadata func (c converter) Ops(status string, msg sdk.Msg) ([]*rosettatypes.Operation, error) { @@ -436,7 +436,7 @@ func sdkEventToBalanceOperations(status string, event abci.Event) (operations [] return operations, true } -// sdkCoinsToRosettaAmounts converts []sdk.Coin to rosetta amounts +// Amounts converts []sdk.Coin to rosetta amounts func (c converter) Amounts(ownedCoins []sdk.Coin, availableCoins sdk.Coins) []*rosettatypes.Amount { amounts := make([]*rosettatypes.Amount, len(availableCoins)) ownedCoinsMap := make(map[string]sdk.Int, len(availableCoins)) @@ -498,7 +498,7 @@ func AddOperationIndexes(msgOps []*rosettatypes.Operation, balanceOps []*rosetta return finalOps } -// endBlockTxHash produces a mock endblock hash that rosetta can query +// EndBlockTxHash produces a mock endblock hash that rosetta can query // for endblock operations, it also serves the purpose of representing // part of the state changes happening at endblock level (balance ones) func (c converter) EndBlockTxHash(hash []byte) string { @@ -506,7 +506,7 @@ func (c converter) EndBlockTxHash(hash []byte) string { return fmt.Sprintf("%X", final) } -// beginBlockTxHash produces a mock beginblock hash that rosetta can query +// BeginBlockTxHash produces a mock beginblock hash that rosetta can query // for beginblock operations, it also serves the purpose of representing // part of the state changes happening at beginblock level (balance ones) func (c converter) BeginBlockTxHash(hash []byte) string { diff --git a/server/rosetta/types.go b/server/rosetta/types.go index c92661a6176c..516706d16c64 100644 --- a/server/rosetta/types.go +++ b/server/rosetta/types.go @@ -11,11 +11,23 @@ const ( StatusPeerSynced = "synced" StatusPeerSyncing = "syncing" ) + +// In rosetta all state transitions must be rapresented as transactions +// since in tendermint begin block and end block are state transitions +// which are not represented as transactions we mock the balance changes +// only happening at those levels as transactions. (check BeginBlockTxHash for more info) +const ( + DeliverTxSize = sha256.Size + BeginEndBlockTxSize = DeliverTxSize + 1 + EndBlockHashStart = 0x0 + BeginBlockHashStart = 0x1 +) + const ( - DeliverTxSize = sha256.Size - BeginEndBlockTxSize = DeliverTxSize + 1 - EndBlockHashStart = 0x0 - BeginBlockHashStart = 0x1 + // BurnerAddressIdentifier mocks the account identifier of a burner address + // all coins burned in the sdk will be sent to this identifier, which per sdk.AccAddress + // design we will never be able to query (as of now). + // Rosetta does not understand supply contraction. BurnerAddressIdentifier = "burner" ) @@ -24,10 +36,10 @@ const ( type TransactionType int const ( - BeginBlockTx TransactionType = iota + UnrecognizedTx TransactionType = iota + BeginBlockTx EndBlockTx DeliverTxTx - UnrecognizedTx ) // metadata options From 0bb7b169315c40793972819b270ce87f8776decd Mon Sep 17 00:00:00 2001 From: Frojdi Dymylja Date: Fri, 5 Mar 2021 21:27:33 +0100 Subject: [PATCH 25/33] chore: address documentation changes --- server/rosetta/types.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/server/rosetta/types.go b/server/rosetta/types.go index 516706d16c64..0d1eada89272 100644 --- a/server/rosetta/types.go +++ b/server/rosetta/types.go @@ -12,10 +12,10 @@ const ( StatusPeerSyncing = "syncing" ) -// In rosetta all state transitions must be rapresented as transactions +// In rosetta all state transitions must be represented as transactions // since in tendermint begin block and end block are state transitions -// which are not represented as transactions we mock the balance changes -// only happening at those levels as transactions. (check BeginBlockTxHash for more info) +// which are not represented as transactions we mock only the balance changes +// happening at those levels as transactions. (check BeginBlockTxHash for more info) const ( DeliverTxSize = sha256.Size BeginEndBlockTxSize = DeliverTxSize + 1 From 57e684adc8985195d5c5d633633c7f6887855cd7 Mon Sep 17 00:00:00 2001 From: Frojdi Dymylja <33157909+fdymylja@users.noreply.github.com> Date: Thu, 11 Mar 2021 12:39:52 +0100 Subject: [PATCH 26/33] Update docs server/rosetta/client_online.go Co-authored-by: Robert Zaremba --- server/rosetta/client_online.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/rosetta/client_online.go b/server/rosetta/client_online.go index 9e18cdeb6182..ea9c7c0ed589 100644 --- a/server/rosetta/client_online.go +++ b/server/rosetta/client_online.go @@ -113,7 +113,7 @@ func (c *Client) Bootstrap() error { return nil } -// Ready asserts if the client is ready or not +// Ready performs a health check and returns an error if the client is not ready. func (c *Client) Ready() error { ctx, cancel := context.WithTimeout(context.Background(), defaultNodeTimeout) defer cancel() From 0c3bad64331b619de164b7f19833eba522a941f1 Mon Sep 17 00:00:00 2001 From: Frojdi Dymylja <33157909+fdymylja@users.noreply.github.com> Date: Thu, 11 Mar 2021 12:40:03 +0100 Subject: [PATCH 27/33] Update docs server/rosetta/client_online.go Co-authored-by: Robert Zaremba --- server/rosetta/client_online.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/rosetta/client_online.go b/server/rosetta/client_online.go index ea9c7c0ed589..19c75192952a 100644 --- a/server/rosetta/client_online.go +++ b/server/rosetta/client_online.go @@ -233,8 +233,8 @@ func (c *Client) TxOperationsAndSignersAccountIdentifiers(signed bool, txBytes [ } } -// GetTx returns a transaction given its hash, in rosetta begin block and end block are mocked -// as transaction hashes in order to adhere to balance tracking rules +// GetTx returns a transaction given its hash. For Rosetta we make a synthetic transaction for BeginBlock +// and EndBlock to adhere to balance tracking rules. func (c *Client) GetTx(ctx context.Context, hash string) (*rosettatypes.Transaction, error) { hashBytes, err := hex.DecodeString(hash) if err != nil { From d6e1622e633042e7947528b1f115e58734994477 Mon Sep 17 00:00:00 2001 From: Frojdi Dymylja <33157909+fdymylja@users.noreply.github.com> Date: Thu, 11 Mar 2021 12:42:14 +0100 Subject: [PATCH 28/33] cleanup spacing server/rosetta/converter_test.go Co-authored-by: Robert Zaremba --- server/rosetta/converter_test.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/server/rosetta/converter_test.go b/server/rosetta/converter_test.go index 3ac0eedf007f..6cc210a59c9f 100644 --- a/server/rosetta/converter_test.go +++ b/server/rosetta/converter_test.go @@ -129,17 +129,14 @@ func (s *ConverterTestSuite) TestMsgToMetaMetaToMsg() { ToAddress: "addr2", Amount: sdk.NewCoins(sdk.NewInt64Coin("test", 10)), } - msg.Route() meta, err := s.c.ToRosetta().Meta(msg) s.Require().NoError(err) copyMsg := new(bank.MsgSend) - err = s.c.ToSDK().Msg(meta, copyMsg) s.Require().NoError(err) - s.Require().Equal(msg, copyMsg) } From f28f18d803301e92ffdd9cf3afac3cd8f51af776 Mon Sep 17 00:00:00 2001 From: Frojdi Dymylja Date: Thu, 11 Mar 2021 12:50:12 +0100 Subject: [PATCH 29/33] revert: baseapp.md --- docs/core/baseapp.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/core/baseapp.md b/docs/core/baseapp.md index a0021e88648c..5d0fd115f3ef 100644 --- a/docs/core/baseapp.md +++ b/docs/core/baseapp.md @@ -104,7 +104,7 @@ Finally, a few more important parameterd: ```go func NewBaseApp( - name string, logger log.Logger, db dbm.DB, txDecode sdk.TxDecoder, options ...func(*BaseApp), + name string, logger log.Logger, db dbm.DB, txDecoder sdk.TxDecoder, options ...func(*BaseApp), ) *BaseApp { // ... From 2f7a5c8204c1a1a6033cb00df058aba8c8b85b6a Mon Sep 17 00:00:00 2001 From: Frojdi Dymylja Date: Thu, 11 Mar 2021 12:55:47 +0100 Subject: [PATCH 30/33] add: docs for interface implementation --- server/rosetta/client_offline.go | 4 ++++ server/rosetta/client_online.go | 6 ++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/server/rosetta/client_offline.go b/server/rosetta/client_offline.go index fb193d0aa584..75fba0a7115d 100644 --- a/server/rosetta/client_offline.go +++ b/server/rosetta/client_offline.go @@ -10,6 +10,8 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" ) +// ---------- cosmos-rosetta-gateway.types.NetworkInformationProvider implementation ------------ // + func (c *Client) OperationStatuses() []*types.OperationStatus { return []*types.OperationStatus{ { @@ -31,6 +33,8 @@ func (c *Client) SupportedOperations() []string { return c.supportedOperations } +// ---------- cosmos-rosetta-gateway.types.OfflineClient implementation ------------ // + func (c *Client) SignedTx(_ context.Context, txBytes []byte, signatures []*types.Signature) (signedTxBytes []byte, err error) { return c.converter.ToSDK().SignedTx(txBytes, signatures) } diff --git a/server/rosetta/client_online.go b/server/rosetta/client_online.go index 19c75192952a..fbb0f98bf789 100644 --- a/server/rosetta/client_online.go +++ b/server/rosetta/client_online.go @@ -91,6 +91,8 @@ func NewClient(cfg *Config) (*Client, error) { }, nil } +// ---------- cosmos-rosetta-gateway.types.Client implementation ------------ // + // Bootstrap is gonna connect the client to the endpoints func (c *Client) Bootstrap() error { grpcConn, err := grpc.Dial(c.config.GRPCEndpoint, grpc.WithInsecure()) @@ -233,8 +235,8 @@ func (c *Client) TxOperationsAndSignersAccountIdentifiers(signed bool, txBytes [ } } -// GetTx returns a transaction given its hash. For Rosetta we make a synthetic transaction for BeginBlock -// and EndBlock to adhere to balance tracking rules. +// GetTx returns a transaction given its hash. For Rosetta we make a synthetic transaction for BeginBlock +// and EndBlock to adhere to balance tracking rules. func (c *Client) GetTx(ctx context.Context, hash string) (*rosettatypes.Transaction, error) { hashBytes, err := hex.DecodeString(hash) if err != nil { From 9fda8a2b93aa6efff468c9551992cc49c181d615 Mon Sep 17 00:00:00 2001 From: Frojdi Dymylja Date: Thu, 11 Mar 2021 12:57:18 +0100 Subject: [PATCH 31/33] remove: converter_test.go utils anonymous type --- server/rosetta/converter_test.go | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/server/rosetta/converter_test.go b/server/rosetta/converter_test.go index 6cc210a59c9f..22d466801686 100644 --- a/server/rosetta/converter_test.go +++ b/server/rosetta/converter_test.go @@ -31,11 +31,9 @@ type ConverterTestSuite struct { unsignedTxBytes []byte unsignedTx authsigning.Tx - util struct { - ir codectypes.InterfaceRegistry - cdc *codec.ProtoCodec - txConf client.TxConfig - } + ir codectypes.InterfaceRegistry + cdc *codec.ProtoCodec + txConf client.TxConfig } func (s *ConverterTestSuite) SetupTest() { @@ -49,11 +47,9 @@ func (s *ConverterTestSuite) SetupTest() { txConfig := authtx.NewTxConfig(cdc, authtx.DefaultSignModes) s.c = rosetta.NewConverter(cdc, ir, txConfig) // add utils - s.util = struct { - ir codectypes.InterfaceRegistry - cdc *codec.ProtoCodec - txConf client.TxConfig - }{ir: ir, cdc: cdc, txConf: txConfig} + s.ir = ir + s.cdc = cdc + s.txConf = txConfig // add authsigning tx sdkTx, err := txConfig.TxDecoder()(unsignedTxBytes) s.Require().NoError(err) @@ -174,11 +170,11 @@ func (s *ConverterTestSuite) TestOpsAndSigners() { Amount: sdk.NewCoins(sdk.NewInt64Coin("test", 10)), } - builder := s.util.txConf.NewTxBuilder() + builder := s.txConf.NewTxBuilder() s.Require().NoError(builder.SetMsgs(msg)) sdkTx := builder.GetTx() - txBytes, err := s.util.txConf.TxEncoder()(sdkTx) + txBytes, err := s.txConf.TxEncoder()(sdkTx) s.Require().NoError(err) ops, signers, err := s.c.ToRosetta().OpsAndSigners(txBytes) From 958fba554a84efcc4cb1f3babe2fe426ba1caea3 Mon Sep 17 00:00:00 2001 From: Frojdi Dymylja Date: Thu, 11 Mar 2021 15:18:35 +0100 Subject: [PATCH 32/33] change: set interface name constant --- server/rosetta/client_online.go | 2 +- types/codec.go | 11 +++++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/server/rosetta/client_online.go b/server/rosetta/client_online.go index fbb0f98bf789..bd122b4069d7 100644 --- a/server/rosetta/client_online.go +++ b/server/rosetta/client_online.go @@ -64,7 +64,7 @@ func NewClient(cfg *Config) (*Client, error) { txConfig := authtx.NewTxConfig(cfg.Codec, authtx.DefaultSignModes) var supportedOperations []string - for _, ii := range cfg.InterfaceRegistry.ListImplementations("cosmos.base.v1beta1.Msg") { + for _, ii := range cfg.InterfaceRegistry.ListImplementations(sdk.MsgInterfaceProtoName) { resolvedMsg, err := cfg.InterfaceRegistry.Resolve(ii) if err != nil { continue diff --git a/types/codec.go b/types/codec.go index 152bb9d724f5..8123fc7d51a4 100644 --- a/types/codec.go +++ b/types/codec.go @@ -5,6 +5,13 @@ import ( "github.com/cosmos/cosmos-sdk/codec/types" ) +const ( + // MsgInterfaceProtoName defines the protobuf name of the cosmos Msg interface + MsgInterfaceProtoName = "cosmos.base.v1beta1.Msg" + // ServiceMsgInterfaceProtoName defines the protobuf name of the cosmos MsgRequest interface + ServiceMsgInterfaceProtoName = "cosmos.base.v1beta1.ServiceMsg" +) + // RegisterLegacyAminoCodec registers the sdk message type. func RegisterLegacyAminoCodec(cdc *codec.LegacyAmino) { cdc.RegisterInterface((*Msg)(nil), nil) @@ -13,8 +20,8 @@ func RegisterLegacyAminoCodec(cdc *codec.LegacyAmino) { // RegisterInterfaces registers the sdk message type. func RegisterInterfaces(registry types.InterfaceRegistry) { - registry.RegisterInterface("cosmos.base.v1beta1.Msg", (*Msg)(nil)) + registry.RegisterInterface(MsgInterfaceProtoName, (*Msg)(nil)) // the interface name for MsgRequest is ServiceMsg because this is most useful for clients // to understand - it will be the way for clients to introspect on available Msg service methods - registry.RegisterInterface("cosmos.base.v1beta1.ServiceMsg", (*MsgRequest)(nil)) + registry.RegisterInterface(ServiceMsgInterfaceProtoName, (*MsgRequest)(nil)) } From 6354cfa4e98ebd4d89b63981235a73746850678b Mon Sep 17 00:00:00 2001 From: Frojdi Dymylja Date: Thu, 11 Mar 2021 15:21:55 +0100 Subject: [PATCH 33/33] chore: add CHANGELOG.md entry --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 39a231c74a5a..f878eabadf4a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,7 +40,7 @@ Ref: https://keepachangelog.com/en/1.0.0/ * [\#8559](https://github.com/cosmos/cosmos-sdk/pull/8559) Added Protobuf compatible secp256r1 ECDSA signatures. * [\#8786](https://github.com/cosmos/cosmos-sdk/pull/8786) Enabled secp256r1 in x/auth. - +* (rosetta) [\#8729](https://github.com/cosmos/cosmos-sdk/pull/8729) Data API fully supports balance tracking. Construction API can now construct any message supported by the application. ### Client Breaking Changes