diff --git a/CHANGELOG.md b/CHANGELOG.md index 39a231c74a5a..f878eabadf4a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,7 +40,7 @@ Ref: https://keepachangelog.com/en/1.0.0/ * [\#8559](https://github.com/cosmos/cosmos-sdk/pull/8559) Added Protobuf compatible secp256r1 ECDSA signatures. * [\#8786](https://github.com/cosmos/cosmos-sdk/pull/8786) Enabled secp256r1 in x/auth. - +* (rosetta) [\#8729](https://github.com/cosmos/cosmos-sdk/pull/8729) Data API fully supports balance tracking. Construction API can now construct any message supported by the application. ### Client Breaking Changes diff --git a/contrib/rosetta/configuration/bootstrap.json b/contrib/rosetta/configuration/bootstrap.json index 1793988f37e5..f75c7ec145bb 100644 --- a/contrib/rosetta/configuration/bootstrap.json +++ b/contrib/rosetta/configuration/bootstrap.json @@ -1,7 +1,7 @@ [ { "account_identifier": { - "address":"cosmos158nkd0l9tyemv2crp579rmj8dg37qty8lzff88" + "address":"cosmos1ujtnemf6jmfm995j000qdry064n5lq854gfe3j" }, "currency":{ "symbol":"stake", diff --git a/contrib/rosetta/configuration/data.sh b/contrib/rosetta/configuration/data.sh index 45297d5a21bf..4d7d5ff0b056 100644 --- a/contrib/rosetta/configuration/data.sh +++ b/contrib/rosetta/configuration/data.sh @@ -45,7 +45,7 @@ sleep 10 # send transaction to deterministic address echo sending transaction with addr $addr -simd tx bank send "$addr" cosmos1wjmt63j4fv9nqda92nsrp2jp2vsukcke4va3pt 100stake --yes --keyring-backend=test --broadcast-mode=block --chain-id=testing +simd tx bank send "$addr" cosmos19g9cm8ymzchq2qkcdv3zgqtwayj9asv3hjv5u5 100stake --yes --keyring-backend=test --broadcast-mode=block --chain-id=testing sleep 10 diff --git a/contrib/rosetta/configuration/rosetta.json b/contrib/rosetta/configuration/rosetta.json index 39a0bb3811dd..b4adc6a756f1 100644 --- a/contrib/rosetta/configuration/rosetta.json +++ b/contrib/rosetta/configuration/rosetta.json @@ -25,7 +25,7 @@ "constructor_dsl_file": "transfer.ros", "end_conditions": { "create_account": 1, - "transfer": 3 + "transfer": 1 } }, "data": { diff --git a/contrib/rosetta/configuration/run_tests.sh b/contrib/rosetta/configuration/run_tests.sh index cd7af92acda2..c53f89ff88a4 100755 --- a/contrib/rosetta/configuration/run_tests.sh +++ b/contrib/rosetta/configuration/run_tests.sh @@ -2,16 +2,6 @@ set -e -addr="abcd" - -send_tx() { - echo '12345678' | simd tx bank send $addr "$1" "$2" -} - -detect_account() { - line=$1 -} - wait_for_rosetta() { timeout 30 sh -c 'until nc -z $0 $1; do sleep 1; done' rosetta 8080 } @@ -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 diff --git a/contrib/rosetta/configuration/staking.json b/contrib/rosetta/configuration/staking.json deleted file mode 100644 index 9c5e5da3ba46..000000000000 --- a/contrib/rosetta/configuration/staking.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "network": { - "blockchain": "app", - "network": "network" - }, - "online_url": "http://rosetta:8080", - "data_directory": "", - "http_timeout": 300, - "max_retries": 5, - "retry_elapsed_time": 0, - "max_online_connections": 0, - "max_sync_concurrency": 0, - "tip_delay": 60, - "log_configuration": true, - "construction": { - "offline_url": "http://rosetta:8080", - "max_offline_connections": 0, - "stale_depth": 0, - "broadcast_limit": 0, - "ignore_broadcast_failures": false, - "clear_broadcasts": false, - "broadcast_behind_tip": false, - "block_broadcast_limit": 0, - "rebroadcast_all": false, - "constructor_dsl_file": "staking.ros", - "end_conditions": { - "staking": 3 - } - } -} \ No newline at end of file diff --git a/contrib/rosetta/configuration/staking.ros b/contrib/rosetta/configuration/staking.ros deleted file mode 100644 index 4f89a43b9893..000000000000 --- a/contrib/rosetta/configuration/staking.ros +++ /dev/null @@ -1,147 +0,0 @@ -request_funds(1){ - find_account{ - currency = {"symbol":"stake", "decimals":0}; - random_account = find_balance({ - "minimum_balance":{ - "value": "0", - "currency": {{currency}} - }, - "create_limit":1 - }); - }, - send_funds{ - account_identifier = {{random_account.account_identifier}}; - address = {{account_identifier.address}}; - idk = http_request({ - "method": "POST", - "url": "http:\/\/faucet:8000", - "timeout": 10, - "body": {{random_account.account_identifier.address}} - }); - }, - // Create a separate scenario to request funds so that - // the address we are using to request funds does not - // get rolled back if funds do not yet exist. - request{ - loaded_account = find_balance({ - "account_identifier": {{random_account.account_identifier}}, - "minimum_balance":{ - "value": "100", - "currency": {{currency}} - } - }); - } -} -create_account(1){ - create{ - network = {"network":"network", "blockchain":"app"}; - key = generate_key({"curve_type": "secp256k1"}); - account = derive({ - "network_identifier": {{network}}, - "public_key": {{key.public_key}} - }); - // If the account is not saved, the key will be lost! - save_account({ - "account_identifier": {{account.account_identifier}}, - "keypair": {{key}} - }); - } -} - -staking(1){ - stake{ - stake.network = {"network":"network", "blockchain":"app"}; - currency = {"symbol":"stake", "decimals":0}; - sender = find_balance({ - "minimum_balance":{ - "value": "100", - "currency": {{currency}} - } - }); - // Set the recipient_amount as some value <= sender.balance-max_fee - max_fee = "0"; - fee_amount = "1"; - fee_value = 0 - {{fee_amount}}; - available_amount = {{sender.balance.value}} - {{max_fee}}; - recipient_amount = "1"; - print_message({"recipient_amount":{{recipient_amount}}}); - // Find recipient and construct operations - recipient = {{sender.account_identifier}}; - sender_amount = 0 - {{recipient_amount}}; - stake.confirmation_depth = "1"; - stake.operations = [ - { - "operation_identifier":{"index":0}, - "type":"fee", - "account":{{sender.account_identifier}}, - "amount":{ - "value":{{fee_value}}, - "currency":{{currency}} - } - }, - { - "operation_identifier":{"index":1}, - "type":"cosmos.staking.v1beta1.MsgDelegate", - "account":{{sender.account_identifier}}, - "amount":{ - "value":{{sender_amount}}, - "currency":{{currency}} - } - }, - { - "operation_identifier":{"index":2}, - "type":"cosmos.staking.v1beta1.MsgDelegate", - "account": { - "address": "staking_account", - "sub_account": { - "address" : "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}} - } - } - ]; - } -} diff --git a/contrib/rosetta/configuration/transfer.ros b/contrib/rosetta/configuration/transfer.ros index a1cb3f8caf89..74ebd2ddf50c 100644 --- a/contrib/rosetta/configuration/transfer.ros +++ b/contrib/rosetta/configuration/transfer.ros @@ -26,7 +26,7 @@ request_funds(1){ loaded_account = find_balance({ "account_identifier": {{random_account.account_identifier}}, "minimum_balance":{ - "value": "100", + "value": "50", "currency": {{currency}} } }); @@ -57,6 +57,8 @@ transfer(3){ "currency": {{currency}} } }); + acc_identifier = {{sender.account_identifier}}; + sender_address = {{acc_identifier.address}}; // Set the recipient_amount as some value <= sender.balance-max_fee max_fee = "0"; fee_amount = "1"; @@ -76,34 +78,28 @@ transfer(3){ "create_probability": 50 }); transfer.confirmation_depth = "1"; + recipient_account_identifier = {{recipient.account_identifier}}; + recipient_address = {{recipient_account_identifier.address}}; transfer.operations = [ { "operation_identifier":{"index":0}, - "type":"fee", - "account":{{sender.account_identifier}}, - "amount":{ - "value":{{fee_value}}, - "currency":{{currency}} - } - }, - { - "operation_identifier":{"index":1}, "type":"cosmos.bank.v1beta1.MsgSend", "account":{{sender.account_identifier}}, - "amount":{ - "value":{{sender_amount}}, - "currency":{{currency}} - } - }, - { - "operation_identifier":{"index":2}, - "type":"cosmos.bank.v1beta1.MsgSend", - "account":{{recipient.account_identifier}}, - "amount":{ - "value":{{recipient_amount}}, - "currency":{{currency}} + "metadata": { + "amount": [ + { + "amount": {{recipient_amount}}, + "denom": {{currency.symbol}} + } + ], + "from_address": {{sender_address}}, + "to_address": {{recipient_address}} } } ]; + transfer.preprocess_metadata = { + "gas_price": "1stake", + "gas_limit": 250000 + }; } } diff --git a/contrib/rosetta/node/data.tar.gz b/contrib/rosetta/node/data.tar.gz index ad285ac62e75..987bb88b33ac 100644 Binary files a/contrib/rosetta/node/data.tar.gz and b/contrib/rosetta/node/data.tar.gz differ diff --git a/go.mod b/go.mod index d409d401313f..b23c929c8d09 100644 --- a/go.mod +++ b/go.mod @@ -45,7 +45,7 @@ require ( github.com/spf13/viper v1.7.1 github.com/stretchr/testify v1.7.0 github.com/tendermint/btcd v0.1.1 - github.com/tendermint/cosmos-rosetta-gateway v0.3.0-rc2 + github.com/tendermint/cosmos-rosetta-gateway v0.3.0-rc2.0.20210304154332-87d6ca4410df github.com/tendermint/crypto v0.0.0-20191022145703-50d29ede1e15 github.com/tendermint/go-amino v0.16.0 github.com/tendermint/tendermint v0.34.8 diff --git a/go.sum b/go.sum index a2c9cd63ed57..06f18fa3feea 100644 --- a/go.sum +++ b/go.sum @@ -660,8 +660,8 @@ github.com/tecbot/gorocksdb v0.0.0-20191217155057-f0fad39f321c h1:g+WoO5jjkqGAzH github.com/tecbot/gorocksdb v0.0.0-20191217155057-f0fad39f321c/go.mod h1:ahpPrc7HpcfEWDQRZEmnXMzHY03mLDYMCxeDzy46i+8= github.com/tendermint/btcd v0.1.1 h1:0VcxPfflS2zZ3RiOAHkBiFUcPvbtRj5O7zHmcJWHV7s= github.com/tendermint/btcd v0.1.1/go.mod h1:DC6/m53jtQzr/NFmMNEu0rxf18/ktVoVtMrnDD5pN+U= -github.com/tendermint/cosmos-rosetta-gateway v0.3.0-rc2 h1:crekJuQ57yIBDuKd3/dMJ00ZvOHURuv9RGJSi2hWTW4= -github.com/tendermint/cosmos-rosetta-gateway v0.3.0-rc2/go.mod h1:gBPw8WV2Erm4UGHlBRiM3zaEBst4bsuihmMCNQdgP/s= +github.com/tendermint/cosmos-rosetta-gateway v0.3.0-rc2.0.20210304154332-87d6ca4410df h1:hoMLrOS4WyyMM+Y+iWdGu94o0zzp6Q43y7v89Q1/OIw= +github.com/tendermint/cosmos-rosetta-gateway v0.3.0-rc2.0.20210304154332-87d6ca4410df/go.mod h1:gBPw8WV2Erm4UGHlBRiM3zaEBst4bsuihmMCNQdgP/s= github.com/tendermint/crypto v0.0.0-20191022145703-50d29ede1e15 h1:hqAk8riJvK4RMWx1aInLzndwxKalgi5rTqgfXxOxbEI= github.com/tendermint/crypto v0.0.0-20191022145703-50d29ede1e15/go.mod h1:z4YtwM70uOnk8h0pjJYlj3zdYwi9l03By6iAIF5j/Pk= github.com/tendermint/go-amino v0.16.0 h1:GyhmgQKvqF82e2oZeuMSp9JTN0N09emoSZlb2lyGa2E= diff --git a/server/rosetta/client_offline.go b/server/rosetta/client_offline.go index f619bfc6d2cf..75fba0a7115d 100644 --- a/server/rosetta/client_offline.go +++ b/server/rosetta/client_offline.go @@ -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 - } - - 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 - } +// ---------- cosmos-rosetta-gateway.types.OfflineClient implementation ------------ // - 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()) + metadata := new(ConstructionMetadata) + if err = metadata.FromMetadata(request.Metadata); err != nil { + return nil, err } - 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...) + txBytes, payloads, err := c.converter.ToRosetta().SigningComponents(tx, metadata, request.PublicKeys) if 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(), - } + return &types.ConstructionPayloadsResponse{ + UnsignedTransaction: hex.EncodeToString(txBytes), + Payloads: payloads, + }, nil +} - signBytes, err := TxConfig.SignModeHandler().GetSignBytes(txFactory.SignMode(), signerData, txBldr.GetTx()) - if err != nil { - return nil, err +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") } - txBytes, err := TxConfig.TxEncoder()(txBldr.GetTx()) + // 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 } - accIdentifiers := getAccountIdentifiersByMsgs(msgs) + // get the signers + signers := tx.GetSigners() + signersStr := make([]string, len(signers)) + accountIdentifiers := make([]*types.AccountIdentifier, len(signers)) - payloads := make([]*types.SigningPayload, len(accIdentifiers)) - for i, accID := range accIdentifiers { - payloads[i] = &types.SigningPayload{ - AccountIdentifier: accID, - Bytes: crypto.Sha256(signBytes), - SignatureType: types.Ecdsa, + for i, sig := range signers { + addr := sig.String() + signersStr[i] = addr + accountIdentifiers[i] = &types.AccountIdentifier{ + Address: addr, } } - - 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()}) - } + // get the metadata request information + meta := new(ConstructionPreprocessMetadata) + err = meta.FromMetadata(req.Metadata) + if err != nil { + return nil, err } - 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") + if meta.GasPrice == "" { + return nil, crgerrs.WrapError(crgerrs.ErrBadArgument, "no gas prices") } - msgs, err := opsToMsgs(c.ir, operations) - if err != nil { - return nil, crgerrs.WrapError(crgerrs.ErrInvalidOperation, err.Error()) + if meta.GasLimit == 0 { + return nil, crgerrs.WrapError(crgerrs.ErrBadArgument, "no gas limit") } - if len(msgs) < 1 || len(msgs[0].GetSigners()) < 1 { - return nil, crgerrs.WrapError(crgerrs.ErrInvalidOperation, "operation produced no msg or signers") + // prepare the options to return + options := &PreprocessOperationsOptionsResponse{ + ExpectedSigners: signersStr, + Memo: meta.Memo, + GasLimit: meta.GasLimit, + GasPrice: meta.GasPrice, } - memo, ok := req.Metadata["memo"] - if !ok { - memo = "" + metaOptions, err := options.ToMetadata() + if err != nil { + return nil, err } + return &types.ConstructionPreprocessResponse{ + Options: metaOptions, + RequiredPublicKeys: accountIdentifiers, + }, nil +} - defaultGas := float64(200000) - - gas := req.SuggestedFeeMultiplier - if gas == nil { - gas = &defaultGas +func (c *Client) AccountIdentifierFromPublicKey(pubKey *types.PublicKey) (*types.AccountIdentifier, error) { + pk, err := c.converter.ToSDK().PubKey(pubKey) + if err != nil { + return nil, err } - return map[string]interface{}{ - OptionAddress: msgs[0].GetSigners()[0], - OptionMemo: memo, - OptionGas: gas, + return &types.AccountIdentifier{ + Address: sdk.AccAddress(pk.Address()).String(), }, nil } diff --git a/server/rosetta/client_online.go b/server/rosetta/client_online.go index f5e5cfafeebf..bd122b4069d7 100644 --- a/server/rosetta/client_online.go +++ b/server/rosetta/client_online.go @@ -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 { - config *Config - - auth auth.QueryClient - bank bank.QueryClient + supportedOperations []string - ir codectypes.InterfaceRegistry + config *Config - 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 +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 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) +// 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, nil, err + return nil, crgerrs.WrapError(crgerrs.ErrCodec, fmt.Sprintf("bad tx hash: %s", err)) } - txBldr, err := txConfig.WrapTxBuilder(rawTx) - if err != nil { - return nil, nil, err - } + // get tx type and hash + txType, hashBytes := c.converter.ToSDK().HashToTxType(hashBytes) - var accountIdentifierSigners []*types.AccountIdentifier - if signed { - addrs := txBldr.GetTx().GetSigners() - for _, addr := range addrs { - signer := &types.AccountIdentifier{ - Address: addr.String(), - } - accountIdentifierSigners = append(accountIdentifierSigners, signer) + // 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()) } - } - return sdkTxToOperations(txBldr.GetTx(), false, false), accountIdentifierSigners, nil -} + // get block txs + fullBlock, err := c.blockTxs(ctx, &block.Block.Height) + if err != nil { + return nil, err + } -// 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) - if err != nil { - return nil, crgerrs.WrapError(crgerrs.ErrUnknown, err.Error()) + 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)) } - var sdkTx sdk.Tx - err = c.ir.UnpackAny(txResp.Tx, &sdkTx) - if err != nil { - return nil, crgerrs.WrapError(crgerrs.ErrCodec, err.Error()) - } - 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 - } + // 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 &types.Transaction{ - TransactionIdentifier: TmTxToRosettaTxsIdentifier(tx), - Operations: sdkTxToOperations(sdkTx, false, false), - Metadata: nil, - }, nil + // iterate over unconfirmed txs to find the one with matching hash + for _, unconfirmedTx := range res.Txs { + if !bytes.Equal(unconfirmedTx.Hash(), hashAsBytes) { + continue } - } - return nil, crgerrs.WrapError(crgerrs.ErrNotFound, "transaction not found in mempool") + return 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 -} - -func (c *Client) getTxConfig() client.TxConfig { - return c.clientCtx.TxConfig + return c.converter.ToRosetta().SyncStatus(status), err } -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") - } - - addrString, ok := addr.(string) - if !ok { - return nil, crgerrs.WrapError(crgerrs.ErrInvalidAddress, "address is not a string") - } + constructionOptions := new(PreprocessOperationsOptionsResponse) - 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 + } - memo, ok := options[OptionMemo] - if !ok { - return nil, crgerrs.WrapError(crgerrs.ErrInvalidMemo, "memo not set") + signersData[i] = accountInfo } - 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, - }, nil + metadataResp := ConstructionMetadata{ + ChainID: status.NodeInfo.Network, + SignersData: signersData, + GasLimit: constructionOptions.GasLimit, + GasPrice: constructionOptions.GasPrice, + Memo: constructionOptions.Memo, + } + + return metadataResp.ToMetadata() } -func (c *Client) Ready() error { - ctx, cancel := context.WithTimeout(context.Background(), defaultNodeTimeout) - defer cancel() - _, err := c.clientCtx.Client.Health(ctx) +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 err + return crgtypes.BlockTransactionsResponse{}, err } - _, err = c.bank.TotalSupply(ctx, &bank.QueryTotalSupplyRequest{}) + // get block events + blockResults, err := c.tmRPC.BlockResults(ctx, height) if err != nil { - return err + return crgtypes.BlockTransactionsResponse{}, err } - return nil -} -func (c *Client) Bootstrap() error { - grpcConn, err := grpc.Dial(c.config.GRPCEndpoint, grpc.WithInsecure()) - if err != nil { - return err + if len(blockResults.TxsResults) != len(blockInfo.Block.Txs) { + // wtf? + panic("block results transactions do now match block transactions") } - - tmRPC, err := http.New(c.config.TendermintRPC, tmWebsocketPath) - if err != nil { - return err + // 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), + ), } - authClient := auth.NewQueryClient(grpcConn) - bankClient := bank.NewQueryClient(grpcConn) + endBlockTx := &rosettatypes.Transaction{ + TransactionIdentifier: &rosettatypes.TransactionIdentifier{Hash: c.converter.ToRosetta().EndBlockTxHash(blockInfo.BlockID.Hash)}, + Operations: AddOperationIndexes( + nil, + c.converter.ToRosetta().BalanceOps(StatusTxSuccess, blockResults.EndBlockEvents), + ), + } - // 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) + 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 + } - c.auth = authClient - c.bank = bankClient - c.clientCtx = clientCtx - c.ir = c.config.InterfaceRegistry + finalTxs := make([]*rosettatypes.Transaction, 0, 2+len(deliverTx)) + finalTxs = append(finalTxs, beginBlockTx) + finalTxs = append(finalTxs, deliverTx...) + finalTxs = append(finalTxs, endBlockTx) - return nil + return crgtypes.BlockTransactionsResponse{ + BlockResponse: c.converter.ToRosetta().BlockResponse(blockInfo), + Transactions: finalTxs, + }, nil } diff --git a/server/rosetta/conv_from_rosetta.go b/server/rosetta/conv_from_rosetta.go deleted file mode 100644 index da9ea5b2ed6f..000000000000 --- a/server/rosetta/conv_from_rosetta.go +++ /dev/null @@ -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()), - } -} diff --git a/server/rosetta/conv_to_rosetta.go b/server/rosetta/conv_to_rosetta.go deleted file mode 100644 index 09146eed4f3e..000000000000 --- a/server/rosetta/conv_to_rosetta.go +++ /dev/null @@ -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 -} diff --git a/server/rosetta/converter.go b/server/rosetta/converter.go new file mode 100644 index 000000000000..43c9460014fc --- /dev/null +++ b/server/rosetta/converter.go @@ -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 +} diff --git a/server/rosetta/converter_test.go b/server/rosetta/converter_test.go new file mode 100644 index 000000000000..22d466801686 --- /dev/null +++ b/server/rosetta/converter_test.go @@ -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)) +} diff --git a/server/rosetta/types.go b/server/rosetta/types.go index 626e7470ab91..0d1eada89272 100644 --- a/server/rosetta/types.go +++ b/server/rosetta/types.go @@ -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" ) -// misc +// 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 ( - Log = "log" + DeliverTxSize = sha256.Size + BeginEndBlockTxSize = DeliverTxSize + 1 + EndBlockHashStart = 0x0 + BeginBlockHashStart = 0x1 ) -// operations const ( - OperationFee = "fee" + // 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" ) -// options +// TransactionType is used to distinguish if a rosetta provided hash +// represents endblock, beginblock or deliver tx +type TransactionType int + const ( - OptionAccountNumber = "account_number" - OptionAddress = "address" - OptionChainID = "chain_id" - OptionSequence = "sequence" - OptionMemo = "memo" - OptionGas = "gas" + UnrecognizedTx TransactionType = iota + BeginBlockTx + EndBlockTx + DeliverTxTx ) -type Msg interface { - sdk.Msg - ToOperations(withStatus, hasError bool) []*types.Operation - FromOperations(ops []*types.Operation) (sdk.Msg, error) +// metadata options + +// misc +const ( + Log = "log" +) + +// ConstructionPreprocessMetadata is used to represent +// the metadata rosetta can provide during preprocess options +type ConstructionPreprocessMetadata struct { + Memo string `json:"memo"` + GasLimit uint64 `json:"gas_limit"` + GasPrice string `json:"gas_price"` +} + +func (c *ConstructionPreprocessMetadata) FromMetadata(meta map[string]interface{}) error { + return unmarshalMetadata(meta, c) +} + +// PreprocessOperationsOptionsResponse is the structured metadata options returned by the preprocess operations endpoint +type PreprocessOperationsOptionsResponse struct { + ExpectedSigners []string `json:"expected_signers"` + Memo string `json:"memo"` + GasLimit uint64 `json:"gas_limit"` + GasPrice string `json:"gas_price"` +} + +func (c PreprocessOperationsOptionsResponse) ToMetadata() (map[string]interface{}, error) { + return marshalMetadata(c) +} + +func (c *PreprocessOperationsOptionsResponse) FromMetadata(meta map[string]interface{}) error { + return unmarshalMetadata(meta, c) +} + +// SignerData contains information on the signers when the request +// is being created, used to populate the account information +type SignerData struct { + AccountNumber uint64 `json:"account_number"` + Sequence uint64 `json:"sequence"` +} + +// ConstructionMetadata are the metadata options used to +// construct a transaction. It is returned by ConstructionMetadataFromOptions +// and fed to ConstructionPayload to process the bytes to sign. +type ConstructionMetadata struct { + ChainID string `json:"chain_id"` + SignersData []*SignerData `json:"signer_data"` + GasLimit uint64 `json:"gas_limit"` + GasPrice string `json:"gas_price"` + Memo string `json:"memo"` +} + +func (c ConstructionMetadata) ToMetadata() (map[string]interface{}, error) { + return marshalMetadata(c) +} + +func (c *ConstructionMetadata) FromMetadata(meta map[string]interface{}) error { + return unmarshalMetadata(meta, c) } diff --git a/server/rosetta/util.go b/server/rosetta/util.go index 29e4a1587dc2..43626b5ed351 100644 --- a/server/rosetta/util.go +++ b/server/rosetta/util.go @@ -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 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") + return crgerrs.WrapError(crgerrs.ErrCodec, err.Error()) } - gasNum, ok := req.Metadata[OptionGas] - if !ok { - return nil, fmt.Errorf("gas metadata was not provided") + err = json.Unmarshal(b, target) + if err != nil { + return crgerrs.WrapError(crgerrs.ErrCodec, err.Error()) } - gasF64, ok := gasNum.(float64) - if !ok { - return nil, fmt.Errorf("invalid gas value") - } + return nil +} - memo, ok := req.Metadata[OptionMemo] - if !ok { - memo = "" +// marshalMetadata marshals the given interface to map[string]interface{} +func marshalMetadata(o interface{}) (meta map[string]interface{}, err error) { + b, err := json.Marshal(o) + if err != nil { + return nil, crgerrs.WrapError(crgerrs.ErrCodec, err.Error()) } - - memoStr, ok := memo.(string) - if !ok { - return nil, fmt.Errorf("invalid memo") + meta = make(map[string]interface{}) + err = json.Unmarshal(b, &meta) + if err != nil { + return nil, err } - return &PayloadReqMetadata{ - ChainID: chainID, - Sequence: uint64(seqNum), - AccountNumber: uint64(accNum), - Gas: uint64(gasF64), - Memo: memoStr, - }, nil + return } diff --git a/types/codec.go b/types/codec.go index 152bb9d724f5..8123fc7d51a4 100644 --- a/types/codec.go +++ b/types/codec.go @@ -5,6 +5,13 @@ import ( "github.com/cosmos/cosmos-sdk/codec/types" ) +const ( + // MsgInterfaceProtoName defines the protobuf name of the cosmos Msg interface + MsgInterfaceProtoName = "cosmos.base.v1beta1.Msg" + // ServiceMsgInterfaceProtoName defines the protobuf name of the cosmos MsgRequest interface + ServiceMsgInterfaceProtoName = "cosmos.base.v1beta1.ServiceMsg" +) + // RegisterLegacyAminoCodec registers the sdk message type. func RegisterLegacyAminoCodec(cdc *codec.LegacyAmino) { cdc.RegisterInterface((*Msg)(nil), nil) @@ -13,8 +20,8 @@ func RegisterLegacyAminoCodec(cdc *codec.LegacyAmino) { // RegisterInterfaces registers the sdk message type. func RegisterInterfaces(registry types.InterfaceRegistry) { - registry.RegisterInterface("cosmos.base.v1beta1.Msg", (*Msg)(nil)) + registry.RegisterInterface(MsgInterfaceProtoName, (*Msg)(nil)) // the interface name for MsgRequest is ServiceMsg because this is most useful for clients // to understand - it will be the way for clients to introspect on available Msg service methods - registry.RegisterInterface("cosmos.base.v1beta1.ServiceMsg", (*MsgRequest)(nil)) + registry.RegisterInterface(ServiceMsgInterfaceProtoName, (*MsgRequest)(nil)) } diff --git a/x/bank/types/msgs.go b/x/bank/types/msgs.go index dd41975468ca..22fdc30b3886 100644 --- a/x/bank/types/msgs.go +++ b/x/bank/types/msgs.go @@ -1,13 +1,6 @@ package types import ( - "fmt" - "strconv" - "strings" - - "github.com/coinbase/rosetta-sdk-go/types" - "github.com/gogo/protobuf/proto" - sdk "github.com/cosmos/cosmos-sdk/types" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" ) @@ -69,83 +62,6 @@ func (msg MsgSend) GetSigners() []sdk.AccAddress { return []sdk.AccAddress{from} } -// Rosetta interface -func (msg *MsgSend) ToOperations(withStatus bool, hasError bool) []*types.Operation { - var operations []*types.Operation - - fromAddress := msg.FromAddress - toAddress := msg.ToAddress - amounts := msg.Amount - if len(amounts) == 0 { - return []*types.Operation{} - } - - coin := amounts[0] - sendOp := func(account, amount string, index int) *types.Operation { - var status string - if withStatus { - status = "Success" - if hasError { - status = "Reverted" - } - } - return &types.Operation{ - OperationIdentifier: &types.OperationIdentifier{ - Index: int64(index), - }, - Type: proto.MessageName(msg), - Status: status, - Account: &types.AccountIdentifier{ - Address: account, - }, - Amount: &types.Amount{ - Value: amount, - Currency: &types.Currency{ - Symbol: coin.Denom, - }, - }, - } - } - operations = append(operations, - sendOp(fromAddress, "-"+coin.Amount.String(), 0), - sendOp(toAddress, coin.Amount.String(), 1), - ) - - return operations -} - -func (msg MsgSend) FromOperations(ops []*types.Operation) (sdk.Msg, error) { - var ( - from, to sdk.AccAddress - sendAmt sdk.Coin - err error - ) - - for _, op := range ops { - if strings.HasPrefix(op.Amount.Value, "-") { - from, err = sdk.AccAddressFromBech32(op.Account.Address) - if err != nil { - return nil, err - } - continue - } - - to, err = sdk.AccAddressFromBech32(op.Account.Address) - if err != nil { - return nil, err - } - - amount, err := strconv.ParseInt(op.Amount.Value, 10, 64) - if err != nil { - return nil, fmt.Errorf("invalid amount") - } - - sendAmt = sdk.NewCoin(op.Amount.Currency.Symbol, sdk.NewInt(amount)) - } - - return NewMsgSend(from, to, sdk.NewCoins(sendAmt)), nil -} - var _ sdk.Msg = &MsgMultiSend{} // NewMsgMultiSend - construct arbitrary multi-in, multi-out send msg. diff --git a/x/distribution/types/msg.go b/x/distribution/types/msg.go index 62c8bfc2abbb..8dc72081e9ac 100644 --- a/x/distribution/types/msg.go +++ b/x/distribution/types/msg.go @@ -2,12 +2,6 @@ package types import ( - "fmt" - - rosettatypes "github.com/coinbase/rosetta-sdk-go/types" - "github.com/gogo/protobuf/proto" - - "github.com/cosmos/cosmos-sdk/server/rosetta" sdk "github.com/cosmos/cosmos-sdk/types" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" ) @@ -96,50 +90,6 @@ func (msg MsgWithdrawDelegatorReward) ValidateBasic() error { return nil } -func (msg *MsgWithdrawDelegatorReward) ToOperations(withStatus, hasError bool) []*rosettatypes.Operation { - - var status string - if withStatus { - status = rosetta.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(), diff --git a/x/staking/types/msg.go b/x/staking/types/msg.go index d1da12386ae1..e397046e5779 100644 --- a/x/staking/types/msg.go +++ b/x/staking/types/msg.go @@ -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 -}