[rosetta] implement balance tracking and redo tx construction (#8729)

Co-authored-by: Alessio Treglia <alessio@tendermint.com>
Co-authored-by: Robert Zaremba <robert@zaremba.ch>
This commit is contained in:
Frojdi Dymylja 2021-03-11 16:01:29 +01:00 committed by GitHub
parent 280ee4f15e
commit 288f8dda4b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 1637 additions and 1453 deletions

View File

@ -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

View File

@ -1,7 +1,7 @@
[
{
"account_identifier": {
"address":"cosmos158nkd0l9tyemv2crp579rmj8dg37qty8lzff88"
"address":"cosmos1ujtnemf6jmfm995j000qdry064n5lq854gfe3j"
},
"currency":{
"symbol":"stake",

View File

@ -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

View File

@ -25,7 +25,7 @@
"constructor_dsl_file": "transfer.ros",
"end_conditions": {
"create_account": 1,
"transfer": 3
"transfer": 1
}
},
"data": {

View File

@ -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
}
@ -25,5 +15,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

View File

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

View File

@ -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" : "cosmosvaloper158nkd0l9tyemv2crp579rmj8dg37qty86kaut5"
}
},
"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" : "cosmosvaloper158nkd0l9tyemv2crp579rmj8dg37qty86kaut5"
}
},
"amount":{
"value":{{sender_amount}},
"currency":{{currency}}
}
}
];
}
}

View File

@ -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
};
}
}

Binary file not shown.

2
go.mod
View File

@ -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

4
go.sum
View File

@ -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=

View File

@ -3,28 +3,23 @@ package rosetta
import (
"context"
"encoding/hex"
"strings"
"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"
)
// ---------- cosmos-rosetta-gateway.types.NetworkInformationProvider implementation ------------ //
func (c *Client) OperationStatuses() []*types.OperationStatus {
return []*types.OperationStatus{
{
Status: StatusSuccess,
Status: StatusTxSuccess,
Successful: true,
},
{
Status: StatusReverted,
Status: StatusTxReverted,
Successful: false,
},
}
@ -35,76 +30,13 @@ 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)
if err != nil {
continue
}
if _, ok := resolve.(Msg); ok {
supportedOperations = append(supportedOperations, strings.TrimLeft(ii, "/"))
}
}
supportedOperations = append(supportedOperations, OperationFee)
return supportedOperations
return c.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
}
// ---------- cosmos-rosetta-gateway.types.OfflineClient implementation ------------ //
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) {
@ -113,109 +45,90 @@ 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.ToSDK().UnsignedTx(request.Operations)
if err != nil {
return nil, crgerrs.WrapError(crgerrs.ErrInvalidOperation, err.Error())
}
metadata, err := getMetadataFromPayloadReq(request)
if 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...)
if err != nil {
metadata := new(ConstructionMetadata)
if err = metadata.FromMetadata(request.Metadata); err != nil {
return nil, err
}
// 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)
}
signerData := authsigning.SignerData{
ChainID: txFactory.ChainID(),
AccountNumber: txFactory.AccountNumber(),
Sequence: txFactory.Sequence(),
}
signBytes, err := TxConfig.SignModeHandler().GetSignBytes(txFactory.SignMode(), signerData, txBldr.GetTx())
txBytes, payloads, err := c.converter.ToRosetta().SigningComponents(tx, metadata, request.PublicKeys)
if err != nil {
return nil, err
}
txBytes, err := TxConfig.TxEncoder()(txBldr.GetTx())
if err != nil {
return nil, err
}
accIdentifiers := getAccountIdentifiersByMsgs(msgs)
payloads := make([]*types.SigningPayload, len(accIdentifiers))
for i, accID := range accIdentifiers {
payloads[i] = &types.SigningPayload{
AccountIdentifier: accID,
Bytes: crypto.Sha256(signBytes),
SignatureType: types.Ecdsa,
}
}
return &types.ConstructionPayloadsResponse{
UnsignedTransaction: hex.EncodeToString(txBytes),
Payloads: payloads,
}, 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) (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.ToSDK().UnsignedTx(req.Operations)
if err != nil {
return nil, err
}
// get the signers
signers := tx.GetSigners()
signersStr := make([]string, len(signers))
accountIdentifiers := make([]*types.AccountIdentifier, len(signers))
for i, sig := range signers {
addr := sig.String()
signersStr[i] = addr
accountIdentifiers[i] = &types.AccountIdentifier{
Address: addr,
}
}
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")
}
msgs, err := opsToMsgs(c.ir, operations)
// 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,
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) {
pk, err := c.converter.ToSDK().PubKey(pubKey)
if err != nil {
return nil, err
}
return &types.AccountIdentifier{
Address: sdk.AccAddress(pk.Address()).String(),
}, nil
}

View File

@ -6,34 +6,29 @@ import (
"encoding/hex"
"fmt"
"strconv"
"strings"
"time"
"github.com/cosmos/cosmos-sdk/version"
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"
rosettatypes "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"
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"
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
@ -44,36 +39,17 @@ const defaultNodeTimeout = 15 * time.Second
// Client implements a single network client to interact with cosmos based chains
type Client struct {
supportedOperations []string
config *Config
auth auth.QueryClient
bank bank.QueryClient
ir codectypes.InterfaceRegistry
clientCtx client.Context
auth auth.QueryClient
bank bank.QueryClient
tmRPC tmrpc.Client
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
converter Converter
}
// NewClient instantiates a new online servicer
@ -85,14 +61,76 @@ func NewClient(cfg *Config) (*Client, error) {
v = "unknown"
}
txConfig := authtx.NewTxConfig(cfg.Codec, authtx.DefaultSignModes)
var supportedOperations []string
for _, ii := range cfg.InterfaceRegistry.ListImplementations(sdk.MsgInterfaceProtoName) {
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,
ir: cfg.InterfaceRegistry,
version: fmt.Sprintf("%s/%s", info.AppName, v),
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) {
// ---------- 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())
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 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()
_, 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)
ctx = metadata.AppendToOutgoingContext(ctx, grpctypes.GRPCBlockHeightHeader, strHeight)
@ -105,16 +143,14 @@ 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)
signerData, err := c.converter.ToRosetta().SignerData(accountInfo.Account)
if err != nil {
return nil, crgerrs.WrapError(crgerrs.ErrCodec, err.Error())
return nil, err
}
return account, nil
return signerData, 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)
@ -132,7 +168,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().Amounts(balance.Balances, availableCoins), nil
}
func (c *Client) BlockByHash(ctx context.Context, hash string) (crgtypes.BlockResponse, error) {
@ -141,64 +177,39 @@ 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{}, err
return crgtypes.BlockResponse{}, crgerrs.WrapError(crgerrs.ErrBadGateway, err.Error())
}
return buildBlockResponse(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)
block, err := c.tmRPC.Block(ctx, height)
if err != nil {
return crgtypes.BlockResponse{}, err
return crgtypes.BlockResponse{}, crgerrs.WrapError(crgerrs.ErrBadGateway, err.Error())
}
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 c.converter.ToRosetta().BlockResponse(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
@ -210,69 +221,80 @@ 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)
if err != nil {
return nil, nil, err
}
txBldr, err := txConfig.WrapTxBuilder(rawTx)
if err != nil {
return nil, nil, err
}
var accountIdentifierSigners []*types.AccountIdentifier
if signed {
addrs := txBldr.GetTx().GetSigners()
for _, addr := range addrs {
signer := &types.AccountIdentifier{
Address: addr.String(),
}
accountIdentifierSigners = append(accountIdentifierSigners, signer)
func (c *Client) TxOperationsAndSignersAccountIdentifiers(signed bool, txBytes []byte) (ops []*rosettatypes.Operation, signers []*rosettatypes.AccountIdentifier, err error) {
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
}
return sdkTxToOperations(txBldr.GetTx(), false, false), accountIdentifierSigners, nil
}
// GetTx returns a transaction given its hash
func (c *Client) GetTx(_ context.Context, hash string) (*types.Transaction, error) {
txResp, err := authtx.QueryTx(c.clientCtx, hash)
// 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 {
return nil, crgerrs.WrapError(crgerrs.ErrUnknown, err.Error())
return nil, crgerrs.WrapError(crgerrs.ErrCodec, fmt.Sprintf("bad tx hash: %s", err))
}
var sdkTx sdk.Tx
err = c.ir.UnpackAny(txResp.Tx, &sdkTx)
if err != nil {
return nil, crgerrs.WrapError(crgerrs.ErrCodec, err.Error())
// get tx type and hash
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.tmRPC.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.tmRPC.Tx(ctx, hashBytes, true)
if err != nil {
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.tmRPC.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 hash provided: %s", hash))
}
return sdkTxWithHashToOperations(&sdkTxWithHash{
HexHash: txResp.TxHash,
Code: txResp.Code,
Log: txResp.RawLog,
Tx: sdkTx,
}), nil
}
// GetUnconfirmedTx gets an unconfirmed transaction given its hash
func (c *Client) GetUnconfirmedTx(ctx context.Context, hash string) (*types.Transaction, error) {
res, err := c.clientCtx.Client.UnconfirmedTxs(ctx, nil)
func (c *Client) GetUnconfirmedTx(ctx context.Context, hash string) (*rosettatypes.Transaction, error) {
res, err := c.tmRPC.UnconfirmedTxs(ctx, nil)
if err != nil {
return nil, crgerrs.WrapError(crgerrs.ErrNotFound, "unconfirmed tx not found")
}
@ -282,165 +304,168 @@ 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
}
return &types.Transaction{
TransactionIdentifier: TmTxToRosettaTxsIdentifier(tx),
Operations: sdkTxToOperations(sdkTx, false, false),
Metadata: nil,
}, nil
}
// 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, "endblock and begin block txs cannot be unconfirmed")
case DeliverTxSize:
break
}
return nil, crgerrs.WrapError(crgerrs.ErrNotFound, "transaction not found in mempool")
// iterate over unconfirmed txs to find the one with matching hash
for _, unconfirmedTx := range res.Txs {
if !bytes.Equal(unconfirmedTx.Hash(), hashAsBytes) {
continue
}
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) {
txs, err := c.clientCtx.Client.UnconfirmedTxs(ctx, nil)
func (c *Client) Mempool(ctx context.Context) ([]*rosettatypes.TransactionIdentifier, error) {
txs, err := c.tmRPC.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) {
netInfo, err := c.clientCtx.Client.NetInfo(ctx)
func (c *Client) Peers(ctx context.Context) ([]*rosettatypes.Peer, error) {
netInfo, err := c.tmRPC.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) {
status, err := c.clientCtx.Client.Status(ctx)
func (c *Client) Status(ctx context.Context) (*rosettatypes.SyncStatus, error) {
status, err := c.tmRPC.Status(ctx)
if err != nil {
return nil, crgerrs.WrapError(crgerrs.ErrUnknown, err.Error())
}
return TMStatusToRosettaSyncStatus(status), err
return c.converter.ToRosetta().SyncStatus(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)
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 &types.TransactionIdentifier{
Hash: res.TxHash,
return &rosettatypes.TransactionIdentifier{
Hash: fmt.Sprintf("%X", res.Hash),
},
map[string]interface{}{
Log: res.RawLog,
Log: res.Log,
}, 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
}
addr, ok := options[OptionAddress]
if !ok {
return nil, crgerrs.WrapError(crgerrs.ErrInvalidAddress, "no address provided")
}
constructionOptions := new(PreprocessOperationsOptionsResponse)
addrString, ok := addr.(string)
if !ok {
return nil, crgerrs.WrapError(crgerrs.ErrInvalidAddress, "address is not a string")
}
accountInfo, err := c.accountInfo(ctx, addrString, nil)
err = constructionOptions.FromMetadata(options)
if err != nil {
return nil, err
}
gas, ok := options[OptionGas]
if !ok {
return nil, crgerrs.WrapError(crgerrs.ErrInvalidAddress, "gas not set")
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] = accountInfo
}
memo, ok := options[OptionMemo]
if !ok {
return nil, crgerrs.WrapError(crgerrs.ErrInvalidMemo, "memo not set")
}
status, err := c.clientCtx.Client.Status(ctx)
status, err := c.tmRPC.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,
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.tmRPC.Block(ctx, height)
if err != nil {
return crgtypes.BlockTransactionsResponse{}, err
}
// get block events
blockResults, err := c.tmRPC.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().BalanceOps(StatusTxSuccess, blockResults.BeginBlockEvents),
),
}
endBlockTx := &rosettatypes.Transaction{
TransactionIdentifier: &rosettatypes.TransactionIdentifier{Hash: c.converter.ToRosetta().EndBlockTxHash(blockInfo.BlockID.Hash)},
Operations: AddOperationIndexes(
nil,
c.converter.ToRosetta().BalanceOps(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
}
func (c *Client) Ready() error {
ctx, cancel := context.WithTimeout(context.Background(), defaultNodeTimeout)
defer cancel()
_, err := c.clientCtx.Client.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)
// 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.clientCtx = clientCtx
c.ir = c.config.InterfaceRegistry
return nil
}

View File

@ -1,211 +0,0 @@
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))
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
}
// 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
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 = StatusSuccess
}
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
}
// 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()),
}
}

View File

@ -1,95 +0,0 @@
package rosetta
import (
"fmt"
"strconv"
"strings"
"github.com/gogo/protobuf/jsonpb"
"github.com/coinbase/rosetta-sdk-go/types"
sdk "github.com/cosmos/cosmos-sdk/types"
)
// 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
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 []*types.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 []*types.Operation) ([]sdk.Msg, error) {
var msgs []sdk.Msg
var operationsByType = make(map[string][]*types.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
}

804
server/rosetta/converter.go Normal file
View File

@ -0,0 +1,804 @@
package rosetta
import (
"bytes"
"encoding/json"
"fmt"
"reflect"
auth "github.com/cosmos/cosmos-sdk/x/auth/types"
"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"
"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"
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"
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
// 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 {
// 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
}
// ToRosettaConverter is an interface that exposes
// 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
// 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)
// 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
Tx(rawTx tmtypes.Tx, txResult *abci.ResponseDeliverTx) (*rosettatypes.Transaction, error)
// TxIdentifiers converts a tendermint tx to transaction identifiers
TxIdentifiers(txs []tmtypes.Tx) []*rosettatypes.TransactionIdentifier
// 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
}
// ToSDKConverter is an interface that exposes
// all the functions used to convert rosetta types
// to tendermint and sdk types
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
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
}
func NewConverter(cdc *codec.ProtoCodec, ir codectypes.InterfaceRegistry, cfg sdkclient.TxConfig) Converter {
return converter{
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 {
return nil, err
}
return crypto.Sha256(bytesToSign), nil
},
ir: ir,
cdc: cdc,
}
}
func (c converter) ToSDK() ToSDKConverter {
return c
}
func (c converter) ToRosetta() ToRosettaConverter {
return c
}
// OpsToUnsignedTx returns all the sdk.Msgs given the operations
func (c converter) UnsignedTx(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.Msg(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
}
// 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 {
return err
}
return c.cdc.UnmarshalJSON(metaBytes, msg)
}
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())
}
err = json.Unmarshal(b, &meta)
if err != nil {
return nil, crgerrs.WrapError(crgerrs.ErrCodec, err.Error())
}
return
}
// 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) {
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.Request, unwrappedMsg.MethodName),
)
}
opName = proto.MessageName(msg)
if opName == "" {
return nil, crgerrs.WrapError(crgerrs.ErrCodec, fmt.Sprintf("unrecognized message type: %T", msg))
}
}
meta, err := c.Meta(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 := 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 = ""
// 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 := c.Ops(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.BalanceOps(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) BalanceOps(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
}
// 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))
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
}
// 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 {
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 {
final := append([]byte{BeginBlockHashStart}, hash...)
return fmt.Sprintf("%X", final)
}
// 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) SyncStatus(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
}
// 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())
}
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...
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
}
// 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
txBytes, err = c.txEncode(builder.GetTx())
if err != nil {
return nil, nil, crgerrs.WrapError(crgerrs.ErrCodec, err.Error())
}
return txBytes, payloadsToSign, nil
}
// 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
}

View File

@ -0,0 +1,348 @@
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"
"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"
sdk "github.com/cosmos/cosmos-sdk/types"
authtx "github.com/cosmos/cosmos-sdk/x/auth/tx"
bank "github.com/cosmos/cosmos-sdk/x/bank/types"
)
type ConverterTestSuite struct {
suite.Suite
c rosetta.Converter
unsignedTxBytes []byte
unsignedTx authsigning.Tx
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 := rosetta.MakeCodec()
txConfig := authtx.NewTxConfig(cdc, authtx.DefaultSignModes)
s.c = rosetta.NewConverter(cdc, ir, txConfig)
// add utils
s.ir = ir
s.cdc = cdc
s.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() {
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().Ops("", msg1)
s.Require().NoError(err)
ops2, err := s.c.ToRosetta().Ops("", msg2)
s.Require().NoError(err)
ops = append(ops, ops2...)
tx, err := s.c.ToSDK().UnsignedTx(ops)
s.Require().NoError(err)
getMsgs := tx.GetMsgs()
s.Require().Equal(2, len(getMsgs))
s.Require().Equal(getMsgs[0], msg1)
s.Require().Equal(getMsgs[1], msg2)
}
func (s *ConverterTestSuite) TestFromRosettaOpsToTxErrors() {
s.Run("unrecognized op", func() {
op := &rosettatypes.Operation{
Type: "non-existent",
}
_, err := s.c.ToSDK().UnsignedTx([]*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.ToSDK().UnsignedTx([]*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().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)
}
func (s *ConverterTestSuite) TestSignedTx() {
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(s.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(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.txConf.NewTxBuilder()
s.Require().NoError(builder.SetMsgs(msg))
sdkTx := builder.GetTx()
txBytes, err := s.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 (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(rosetta.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(rosetta.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(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(rosetta.UnrecognizedTx, txType)
s.Require().Nil(hash)
txType, hash = s.c.ToSDK().HashToTxType(append([]byte{0x3}, deliverTxBytes...))
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, &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, &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,
&rosetta.ConstructionMetadata{GasPrice: "10stake", SignersData: []*rosetta.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,
&rosetta.ConstructionMetadata{GasPrice: "10stake", SignersData: []*rosetta.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,
&rosetta.ConstructionMetadata{GasPrice: "10stake", SignersData: []*rosetta.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))
}

View File

@ -1,41 +1,104 @@
package rosetta
import (
"github.com/coinbase/rosetta-sdk-go/types"
sdk "github.com/cosmos/cosmos-sdk/types"
"crypto/sha256"
)
// statuses
const (
StatusSuccess = "Success"
StatusReverted = "Reverted"
StageSynced = "synced"
StageSyncing = "syncing"
StatusTxSuccess = "Success"
StatusTxReverted = "Reverted"
StatusPeerSynced = "synced"
StatusPeerSyncing = "syncing"
)
// 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 only the balance changes
// happening at those levels as transactions. (check BeginBlockTxHash for more info)
const (
DeliverTxSize = sha256.Size
BeginEndBlockTxSize = DeliverTxSize + 1
EndBlockHashStart = 0x0
BeginBlockHashStart = 0x1
)
const (
// 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"
)
// TransactionType is used to distinguish if a rosetta provided hash
// represents endblock, beginblock or deliver tx
type TransactionType int
const (
UnrecognizedTx TransactionType = iota
BeginBlockTx
EndBlockTx
DeliverTxTx
)
// metadata options
// misc
const (
Log = "log"
)
// operations
const (
OperationFee = "fee"
)
// options
const (
OptionAccountNumber = "account_number"
OptionAddress = "address"
OptionChainID = "chain_id"
OptionSequence = "sequence"
OptionMemo = "memo"
OptionGas = "gas"
)
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)
}

View File

@ -1,112 +1,43 @@
package rosetta
import (
"fmt"
"encoding/json"
"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"
crgerrs "github.com/tendermint/cosmos-rosetta-gateway/errors"
)
// 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
// timeToMilliseconds converts time to milliseconds timestamp
func timeToMilliseconds(t time.Time) int64 {
return t.UnixNano() / (int64(time.Millisecond) / int64(time.Nanosecond))
}
func tmTxToSdkTx(decode sdk.TxDecoder, tx tmtypes.Tx) (sdk.Tx, error) {
sdkTx, err := decode(tx)
// 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())
}
err = json.Unmarshal(b, target)
if err != nil {
return crgerrs.WrapError(crgerrs.ErrCodec, err.Error())
}
return nil
}
// 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())
}
meta = make(map[string]interface{})
err = json.Unmarshal(b, &meta)
if err != nil {
return nil, err
}
return sdkTx, err
}
type sdkTxWithHash struct {
HexHash string
Code uint32
Log string
Tx sdk.Tx
}
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")
}
accountNum, ok := req.Metadata[OptionAccountNumber]
if !ok {
return nil, fmt.Errorf("account_number metadata was not provided")
}
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")
}
memo, ok := req.Metadata[OptionMemo]
if !ok {
memo = ""
}
memoStr, ok := memo.(string)
if !ok {
return nil, fmt.Errorf("invalid memo")
}
return &PayloadReqMetadata{
ChainID: chainID,
Sequence: uint64(seqNum),
AccountNumber: uint64(accNum),
Gas: uint64(gasF64),
Memo: memoStr,
}, nil
return
}

View File

@ -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))
}

View File

@ -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.

View File

@ -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.StatusSuccess
if hasError {
status = rosetta.StatusReverted
}
}
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(),

View File

@ -2,17 +2,9 @@ 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 +256,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.StatusSuccess
if hasError {
status = rosetta.StatusReverted
}
}
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 +311,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.StatusSuccess
if hasError {
status = rosetta.StatusReverted
}
}
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 +358,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.StatusSuccess
if hasError {
status = rosetta.StatusReverted
}
}
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
}