[rosetta] implement balance tracking and redo tx construction (#8729)
Co-authored-by: Alessio Treglia <alessio@tendermint.com> Co-authored-by: Robert Zaremba <robert@zaremba.ch>
This commit is contained in:
parent
280ee4f15e
commit
288f8dda4b
@ -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
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
[
|
||||
{
|
||||
"account_identifier": {
|
||||
"address":"cosmos158nkd0l9tyemv2crp579rmj8dg37qty8lzff88"
|
||||
"address":"cosmos1ujtnemf6jmfm995j000qdry064n5lq854gfe3j"
|
||||
},
|
||||
"currency":{
|
||||
"symbol":"stake",
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -25,7 +25,7 @@
|
||||
"constructor_dsl_file": "transfer.ros",
|
||||
"end_conditions": {
|
||||
"create_account": 1,
|
||||
"transfer": 3
|
||||
"transfer": 1
|
||||
}
|
||||
},
|
||||
"data": {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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}}
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -26,7 +26,7 @@ request_funds(1){
|
||||
loaded_account = find_balance({
|
||||
"account_identifier": {{random_account.account_identifier}},
|
||||
"minimum_balance":{
|
||||
"value": "100",
|
||||
"value": "50",
|
||||
"currency": {{currency}}
|
||||
}
|
||||
});
|
||||
@ -57,6 +57,8 @@ transfer(3){
|
||||
"currency": {{currency}}
|
||||
}
|
||||
});
|
||||
acc_identifier = {{sender.account_identifier}};
|
||||
sender_address = {{acc_identifier.address}};
|
||||
// Set the recipient_amount as some value <= sender.balance-max_fee
|
||||
max_fee = "0";
|
||||
fee_amount = "1";
|
||||
@ -76,34 +78,28 @@ transfer(3){
|
||||
"create_probability": 50
|
||||
});
|
||||
transfer.confirmation_depth = "1";
|
||||
recipient_account_identifier = {{recipient.account_identifier}};
|
||||
recipient_address = {{recipient_account_identifier.address}};
|
||||
transfer.operations = [
|
||||
{
|
||||
"operation_identifier":{"index":0},
|
||||
"type":"fee",
|
||||
"account":{{sender.account_identifier}},
|
||||
"amount":{
|
||||
"value":{{fee_value}},
|
||||
"currency":{{currency}}
|
||||
}
|
||||
},
|
||||
{
|
||||
"operation_identifier":{"index":1},
|
||||
"type":"cosmos.bank.v1beta1.MsgSend",
|
||||
"account":{{sender.account_identifier}},
|
||||
"amount":{
|
||||
"value":{{sender_amount}},
|
||||
"currency":{{currency}}
|
||||
}
|
||||
},
|
||||
{
|
||||
"operation_identifier":{"index":2},
|
||||
"type":"cosmos.bank.v1beta1.MsgSend",
|
||||
"account":{{recipient.account_identifier}},
|
||||
"amount":{
|
||||
"value":{{recipient_amount}},
|
||||
"currency":{{currency}}
|
||||
"metadata": {
|
||||
"amount": [
|
||||
{
|
||||
"amount": {{recipient_amount}},
|
||||
"denom": {{currency.symbol}}
|
||||
}
|
||||
],
|
||||
"from_address": {{sender_address}},
|
||||
"to_address": {{recipient_address}}
|
||||
}
|
||||
}
|
||||
];
|
||||
transfer.preprocess_metadata = {
|
||||
"gas_price": "1stake",
|
||||
"gas_limit": 250000
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
2
go.mod
2
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
|
||||
|
||||
4
go.sum
4
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=
|
||||
|
||||
@ -3,28 +3,23 @@ package rosetta
|
||||
import (
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"strings"
|
||||
|
||||
"github.com/btcsuite/btcd/btcec"
|
||||
"github.com/coinbase/rosetta-sdk-go/types"
|
||||
crgerrs "github.com/tendermint/cosmos-rosetta-gateway/errors"
|
||||
"github.com/tendermint/tendermint/crypto"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/client/tx"
|
||||
"github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1"
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
"github.com/cosmos/cosmos-sdk/types/tx/signing"
|
||||
authsigning "github.com/cosmos/cosmos-sdk/x/auth/signing"
|
||||
)
|
||||
|
||||
// ---------- cosmos-rosetta-gateway.types.NetworkInformationProvider implementation ------------ //
|
||||
|
||||
func (c *Client) OperationStatuses() []*types.OperationStatus {
|
||||
return []*types.OperationStatus{
|
||||
{
|
||||
Status: StatusSuccess,
|
||||
Status: StatusTxSuccess,
|
||||
Successful: true,
|
||||
},
|
||||
{
|
||||
Status: StatusReverted,
|
||||
Status: StatusTxReverted,
|
||||
Successful: false,
|
||||
},
|
||||
}
|
||||
@ -35,76 +30,13 @@ func (c *Client) Version() string {
|
||||
}
|
||||
|
||||
func (c *Client) SupportedOperations() []string {
|
||||
var supportedOperations []string
|
||||
for _, ii := range c.ir.ListImplementations("cosmos.base.v1beta1.Msg") {
|
||||
resolve, err := c.ir.Resolve(ii)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if _, ok := resolve.(Msg); ok {
|
||||
supportedOperations = append(supportedOperations, strings.TrimLeft(ii, "/"))
|
||||
}
|
||||
}
|
||||
|
||||
supportedOperations = append(supportedOperations, OperationFee)
|
||||
|
||||
return supportedOperations
|
||||
return c.supportedOperations
|
||||
}
|
||||
|
||||
func (c *Client) SignedTx(ctx context.Context, txBytes []byte, signatures []*types.Signature) (signedTxBytes []byte, err error) {
|
||||
TxConfig := c.getTxConfig()
|
||||
rawTx, err := TxConfig.TxDecoder()(txBytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// ---------- cosmos-rosetta-gateway.types.OfflineClient implementation ------------ //
|
||||
|
||||
txBldr, err := TxConfig.WrapTxBuilder(rawTx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var sigs = make([]signing.SignatureV2, len(signatures))
|
||||
for i, signature := range signatures {
|
||||
if signature.PublicKey.CurveType != types.Secp256k1 {
|
||||
return nil, crgerrs.ErrUnsupportedCurve
|
||||
}
|
||||
|
||||
cmp, err := btcec.ParsePubKey(signature.PublicKey.Bytes, btcec.S256())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
compressedPublicKey := make([]byte, secp256k1.PubKeySize)
|
||||
copy(compressedPublicKey, cmp.SerializeCompressed())
|
||||
pubKey := &secp256k1.PubKey{Key: compressedPublicKey}
|
||||
|
||||
accountInfo, err := c.accountInfo(ctx, sdk.AccAddress(pubKey.Address()).String(), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sig := signing.SignatureV2{
|
||||
PubKey: pubKey,
|
||||
Data: &signing.SingleSignatureData{
|
||||
SignMode: signing.SignMode_SIGN_MODE_LEGACY_AMINO_JSON,
|
||||
Signature: signature.Bytes,
|
||||
},
|
||||
Sequence: accountInfo.GetSequence(),
|
||||
}
|
||||
sigs[i] = sig
|
||||
}
|
||||
|
||||
if err = txBldr.SetSignatures(sigs...); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
txBytes, err = c.getTxConfig().TxEncoder()(txBldr.GetTx())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return txBytes, nil
|
||||
func (c *Client) SignedTx(_ context.Context, txBytes []byte, signatures []*types.Signature) (signedTxBytes []byte, err error) {
|
||||
return c.converter.ToSDK().SignedTx(txBytes, signatures)
|
||||
}
|
||||
|
||||
func (c *Client) ConstructionPayload(_ context.Context, request *types.ConstructionPayloadsRequest) (resp *types.ConstructionPayloadsResponse, err error) {
|
||||
@ -113,109 +45,90 @@ func (c *Client) ConstructionPayload(_ context.Context, request *types.Construct
|
||||
return nil, crgerrs.WrapError(crgerrs.ErrInvalidOperation, "expected at least one operation")
|
||||
}
|
||||
|
||||
// convert rosetta operations to sdk msgs and fees (if present)
|
||||
msgs, fee, err := opsToMsgsAndFees(c.ir, request.Operations)
|
||||
tx, err := c.converter.ToSDK().UnsignedTx(request.Operations)
|
||||
if err != nil {
|
||||
return nil, crgerrs.WrapError(crgerrs.ErrInvalidOperation, err.Error())
|
||||
}
|
||||
|
||||
metadata, err := getMetadataFromPayloadReq(request)
|
||||
if err != nil {
|
||||
return nil, crgerrs.WrapError(crgerrs.ErrBadArgument, err.Error())
|
||||
}
|
||||
|
||||
txFactory := tx.Factory{}.WithAccountNumber(metadata.AccountNumber).WithChainID(metadata.ChainID).
|
||||
WithGas(metadata.Gas).WithSequence(metadata.Sequence).WithMemo(metadata.Memo).WithFees(fee.String())
|
||||
|
||||
TxConfig := c.getTxConfig()
|
||||
txFactory = txFactory.WithTxConfig(TxConfig)
|
||||
|
||||
txBldr, err := tx.BuildUnsignedTx(txFactory, msgs...)
|
||||
if err != nil {
|
||||
metadata := new(ConstructionMetadata)
|
||||
if err = metadata.FromMetadata(request.Metadata); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Sign_mode_legacy_amino is being used as default here, as sign_mode_direct
|
||||
// needs the signer infos to be set before hand but rosetta doesn't have a way
|
||||
// to do this yet. To be revisited in future versions of sdk and rosetta
|
||||
if txFactory.SignMode() == signing.SignMode_SIGN_MODE_UNSPECIFIED {
|
||||
txFactory = txFactory.WithSignMode(signing.SignMode_SIGN_MODE_LEGACY_AMINO_JSON)
|
||||
}
|
||||
|
||||
signerData := authsigning.SignerData{
|
||||
ChainID: txFactory.ChainID(),
|
||||
AccountNumber: txFactory.AccountNumber(),
|
||||
Sequence: txFactory.Sequence(),
|
||||
}
|
||||
|
||||
signBytes, err := TxConfig.SignModeHandler().GetSignBytes(txFactory.SignMode(), signerData, txBldr.GetTx())
|
||||
txBytes, payloads, err := c.converter.ToRosetta().SigningComponents(tx, metadata, request.PublicKeys)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
txBytes, err := TxConfig.TxEncoder()(txBldr.GetTx())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
accIdentifiers := getAccountIdentifiersByMsgs(msgs)
|
||||
|
||||
payloads := make([]*types.SigningPayload, len(accIdentifiers))
|
||||
for i, accID := range accIdentifiers {
|
||||
payloads[i] = &types.SigningPayload{
|
||||
AccountIdentifier: accID,
|
||||
Bytes: crypto.Sha256(signBytes),
|
||||
SignatureType: types.Ecdsa,
|
||||
}
|
||||
}
|
||||
|
||||
return &types.ConstructionPayloadsResponse{
|
||||
UnsignedTransaction: hex.EncodeToString(txBytes),
|
||||
Payloads: payloads,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func getAccountIdentifiersByMsgs(msgs []sdk.Msg) []*types.AccountIdentifier {
|
||||
var accIdentifiers []*types.AccountIdentifier
|
||||
for _, msg := range msgs {
|
||||
for _, signer := range msg.GetSigners() {
|
||||
accIdentifiers = append(accIdentifiers, &types.AccountIdentifier{Address: signer.String()})
|
||||
func (c *Client) PreprocessOperationsToOptions(_ context.Context, req *types.ConstructionPreprocessRequest) (response *types.ConstructionPreprocessResponse, err error) {
|
||||
if len(req.Operations) == 0 {
|
||||
return nil, crgerrs.WrapError(crgerrs.ErrBadArgument, "no operations")
|
||||
}
|
||||
|
||||
// now we need to parse the operations to cosmos sdk messages
|
||||
tx, err := c.converter.ToSDK().UnsignedTx(req.Operations)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// get the signers
|
||||
signers := tx.GetSigners()
|
||||
signersStr := make([]string, len(signers))
|
||||
accountIdentifiers := make([]*types.AccountIdentifier, len(signers))
|
||||
|
||||
for i, sig := range signers {
|
||||
addr := sig.String()
|
||||
signersStr[i] = addr
|
||||
accountIdentifiers[i] = &types.AccountIdentifier{
|
||||
Address: addr,
|
||||
}
|
||||
}
|
||||
|
||||
return accIdentifiers
|
||||
}
|
||||
|
||||
func (c *Client) PreprocessOperationsToOptions(_ context.Context, req *types.ConstructionPreprocessRequest) (options map[string]interface{}, err error) {
|
||||
operations := req.Operations
|
||||
if len(operations) < 1 {
|
||||
return nil, crgerrs.WrapError(crgerrs.ErrBadArgument, "invalid number of operations")
|
||||
}
|
||||
|
||||
msgs, err := opsToMsgs(c.ir, operations)
|
||||
// get the metadata request information
|
||||
meta := new(ConstructionPreprocessMetadata)
|
||||
err = meta.FromMetadata(req.Metadata)
|
||||
if err != nil {
|
||||
return nil, crgerrs.WrapError(crgerrs.ErrInvalidOperation, err.Error())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(msgs) < 1 || len(msgs[0].GetSigners()) < 1 {
|
||||
return nil, crgerrs.WrapError(crgerrs.ErrInvalidOperation, "operation produced no msg or signers")
|
||||
if meta.GasPrice == "" {
|
||||
return nil, crgerrs.WrapError(crgerrs.ErrBadArgument, "no gas prices")
|
||||
}
|
||||
|
||||
memo, ok := req.Metadata["memo"]
|
||||
if !ok {
|
||||
memo = ""
|
||||
if meta.GasLimit == 0 {
|
||||
return nil, crgerrs.WrapError(crgerrs.ErrBadArgument, "no gas limit")
|
||||
}
|
||||
|
||||
defaultGas := float64(200000)
|
||||
|
||||
gas := req.SuggestedFeeMultiplier
|
||||
if gas == nil {
|
||||
gas = &defaultGas
|
||||
// prepare the options to return
|
||||
options := &PreprocessOperationsOptionsResponse{
|
||||
ExpectedSigners: signersStr,
|
||||
Memo: meta.Memo,
|
||||
GasLimit: meta.GasLimit,
|
||||
GasPrice: meta.GasPrice,
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
OptionAddress: msgs[0].GetSigners()[0],
|
||||
OptionMemo: memo,
|
||||
OptionGas: gas,
|
||||
metaOptions, err := options.ToMetadata()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &types.ConstructionPreprocessResponse{
|
||||
Options: metaOptions,
|
||||
RequiredPublicKeys: accountIdentifiers,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *Client) AccountIdentifierFromPublicKey(pubKey *types.PublicKey) (*types.AccountIdentifier, error) {
|
||||
pk, err := c.converter.ToSDK().PubKey(pubKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &types.AccountIdentifier{
|
||||
Address: sdk.AccAddress(pk.Address()).String(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
@ -6,34 +6,29 @@ import (
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/version"
|
||||
|
||||
abcitypes "github.com/tendermint/tendermint/abci/types"
|
||||
|
||||
"github.com/tendermint/btcd/btcec"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1"
|
||||
|
||||
"github.com/coinbase/rosetta-sdk-go/types"
|
||||
rosettatypes "github.com/coinbase/rosetta-sdk-go/types"
|
||||
"google.golang.org/grpc/metadata"
|
||||
|
||||
"github.com/tendermint/tendermint/rpc/client/http"
|
||||
tmtypes "github.com/tendermint/tendermint/rpc/core/types"
|
||||
"google.golang.org/grpc"
|
||||
|
||||
crgerrs "github.com/tendermint/cosmos-rosetta-gateway/errors"
|
||||
crgtypes "github.com/tendermint/cosmos-rosetta-gateway/types"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/client"
|
||||
"github.com/cosmos/cosmos-sdk/client/flags"
|
||||
codectypes "github.com/cosmos/cosmos-sdk/codec/types"
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
grpctypes "github.com/cosmos/cosmos-sdk/types/grpc"
|
||||
authtx "github.com/cosmos/cosmos-sdk/x/auth/tx"
|
||||
auth "github.com/cosmos/cosmos-sdk/x/auth/types"
|
||||
bank "github.com/cosmos/cosmos-sdk/x/bank/types"
|
||||
|
||||
tmrpc "github.com/tendermint/tendermint/rpc/client"
|
||||
)
|
||||
|
||||
// interface assertion
|
||||
@ -44,36 +39,17 @@ const defaultNodeTimeout = 15 * time.Second
|
||||
|
||||
// Client implements a single network client to interact with cosmos based chains
|
||||
type Client struct {
|
||||
supportedOperations []string
|
||||
|
||||
config *Config
|
||||
|
||||
auth auth.QueryClient
|
||||
bank bank.QueryClient
|
||||
|
||||
ir codectypes.InterfaceRegistry
|
||||
|
||||
clientCtx client.Context
|
||||
auth auth.QueryClient
|
||||
bank bank.QueryClient
|
||||
tmRPC tmrpc.Client
|
||||
|
||||
version string
|
||||
}
|
||||
|
||||
func (c *Client) AccountIdentifierFromPublicKey(pubKey *types.PublicKey) (*types.AccountIdentifier, error) {
|
||||
if pubKey.CurveType != "secp256k1" {
|
||||
return nil, crgerrs.WrapError(crgerrs.ErrUnsupportedCurve, "only secp256k1 supported")
|
||||
}
|
||||
|
||||
cmp, err := btcec.ParsePubKey(pubKey.Bytes, btcec.S256())
|
||||
if err != nil {
|
||||
return nil, crgerrs.WrapError(crgerrs.ErrBadArgument, err.Error())
|
||||
}
|
||||
|
||||
compressedPublicKey := make([]byte, secp256k1.PubKeySize)
|
||||
copy(compressedPublicKey, cmp.SerializeCompressed())
|
||||
|
||||
pk := secp256k1.PubKey{Key: compressedPublicKey}
|
||||
|
||||
return &types.AccountIdentifier{
|
||||
Address: sdk.AccAddress(pk.Address()).String(),
|
||||
}, nil
|
||||
converter Converter
|
||||
}
|
||||
|
||||
// NewClient instantiates a new online servicer
|
||||
@ -85,14 +61,76 @@ func NewClient(cfg *Config) (*Client, error) {
|
||||
v = "unknown"
|
||||
}
|
||||
|
||||
txConfig := authtx.NewTxConfig(cfg.Codec, authtx.DefaultSignModes)
|
||||
|
||||
var supportedOperations []string
|
||||
for _, ii := range cfg.InterfaceRegistry.ListImplementations(sdk.MsgInterfaceProtoName) {
|
||||
resolvedMsg, err := cfg.InterfaceRegistry.Resolve(ii)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if _, ok := resolvedMsg.(sdk.Msg); ok {
|
||||
supportedOperations = append(supportedOperations, strings.TrimLeft(ii, "/"))
|
||||
}
|
||||
}
|
||||
|
||||
supportedOperations = append(
|
||||
supportedOperations,
|
||||
bank.EventTypeCoinSpent, bank.EventTypeCoinReceived,
|
||||
)
|
||||
|
||||
return &Client{
|
||||
config: cfg,
|
||||
ir: cfg.InterfaceRegistry,
|
||||
version: fmt.Sprintf("%s/%s", info.AppName, v),
|
||||
supportedOperations: supportedOperations,
|
||||
config: cfg,
|
||||
auth: nil,
|
||||
bank: nil,
|
||||
tmRPC: nil,
|
||||
version: fmt.Sprintf("%s/%s", info.AppName, v),
|
||||
converter: NewConverter(cfg.Codec, cfg.InterfaceRegistry, txConfig),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *Client) accountInfo(ctx context.Context, addr string, height *int64) (auth.AccountI, error) {
|
||||
// ---------- cosmos-rosetta-gateway.types.Client implementation ------------ //
|
||||
|
||||
// Bootstrap is gonna connect the client to the endpoints
|
||||
func (c *Client) Bootstrap() error {
|
||||
grpcConn, err := grpc.Dial(c.config.GRPCEndpoint, grpc.WithInsecure())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tmRPC, err := http.New(c.config.TendermintRPC, tmWebsocketPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
authClient := auth.NewQueryClient(grpcConn)
|
||||
bankClient := bank.NewQueryClient(grpcConn)
|
||||
|
||||
c.auth = authClient
|
||||
c.bank = bankClient
|
||||
c.tmRPC = tmRPC
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Ready performs a health check and returns an error if the client is not ready.
|
||||
func (c *Client) Ready() error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultNodeTimeout)
|
||||
defer cancel()
|
||||
_, err := c.tmRPC.Health(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = c.bank.TotalSupply(ctx, &bank.QueryTotalSupplyRequest{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) accountInfo(ctx context.Context, addr string, height *int64) (*SignerData, error) {
|
||||
if height != nil {
|
||||
strHeight := strconv.FormatInt(*height, 10)
|
||||
ctx = metadata.AppendToOutgoingContext(ctx, grpctypes.GRPCBlockHeightHeader, strHeight)
|
||||
@ -105,16 +143,14 @@ func (c *Client) accountInfo(ctx context.Context, addr string, height *int64) (a
|
||||
return nil, crgerrs.FromGRPCToRosettaError(err)
|
||||
}
|
||||
|
||||
var account auth.AccountI
|
||||
err = c.ir.UnpackAny(accountInfo.Account, &account)
|
||||
signerData, err := c.converter.ToRosetta().SignerData(accountInfo.Account)
|
||||
if err != nil {
|
||||
return nil, crgerrs.WrapError(crgerrs.ErrCodec, err.Error())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return account, nil
|
||||
return signerData, nil
|
||||
}
|
||||
|
||||
func (c *Client) Balances(ctx context.Context, addr string, height *int64) ([]*types.Amount, error) {
|
||||
func (c *Client) Balances(ctx context.Context, addr string, height *int64) ([]*rosettatypes.Amount, error) {
|
||||
if height != nil {
|
||||
strHeight := strconv.FormatInt(*height, 10)
|
||||
ctx = metadata.AppendToOutgoingContext(ctx, grpctypes.GRPCBlockHeightHeader, strHeight)
|
||||
@ -132,7 +168,7 @@ func (c *Client) Balances(ctx context.Context, addr string, height *int64) ([]*t
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return sdkCoinsToRosettaAmounts(balance.Balances, availableCoins), nil
|
||||
return c.converter.ToRosetta().Amounts(balance.Balances, availableCoins), nil
|
||||
}
|
||||
|
||||
func (c *Client) BlockByHash(ctx context.Context, hash string) (crgtypes.BlockResponse, error) {
|
||||
@ -141,64 +177,39 @@ func (c *Client) BlockByHash(ctx context.Context, hash string) (crgtypes.BlockRe
|
||||
return crgtypes.BlockResponse{}, fmt.Errorf("invalid block hash: %s", err)
|
||||
}
|
||||
|
||||
block, err := c.clientCtx.Client.BlockByHash(ctx, bHash)
|
||||
block, err := c.tmRPC.BlockByHash(ctx, bHash)
|
||||
if err != nil {
|
||||
return crgtypes.BlockResponse{}, err
|
||||
return crgtypes.BlockResponse{}, crgerrs.WrapError(crgerrs.ErrBadGateway, err.Error())
|
||||
}
|
||||
|
||||
return buildBlockResponse(block), nil
|
||||
return c.converter.ToRosetta().BlockResponse(block), nil
|
||||
}
|
||||
|
||||
func (c *Client) BlockByHeight(ctx context.Context, height *int64) (crgtypes.BlockResponse, error) {
|
||||
block, err := c.clientCtx.Client.Block(ctx, height)
|
||||
block, err := c.tmRPC.Block(ctx, height)
|
||||
if err != nil {
|
||||
return crgtypes.BlockResponse{}, err
|
||||
return crgtypes.BlockResponse{}, crgerrs.WrapError(crgerrs.ErrBadGateway, err.Error())
|
||||
}
|
||||
|
||||
return buildBlockResponse(block), nil
|
||||
}
|
||||
|
||||
func buildBlockResponse(block *tmtypes.ResultBlock) crgtypes.BlockResponse {
|
||||
return crgtypes.BlockResponse{
|
||||
Block: TMBlockToRosettaBlockIdentifier(block),
|
||||
ParentBlock: TMBlockToRosettaParentBlockIdentifier(block),
|
||||
MillisecondTimestamp: timeToMilliseconds(block.Block.Time),
|
||||
TxCount: int64(len(block.Block.Txs)),
|
||||
}
|
||||
return c.converter.ToRosetta().BlockResponse(block), nil
|
||||
}
|
||||
|
||||
func (c *Client) BlockTransactionsByHash(ctx context.Context, hash string) (crgtypes.BlockTransactionsResponse, error) {
|
||||
// TODO(fdymylja): use a faster path, by searching the block by hash, instead of doing a double query operation
|
||||
blockResp, err := c.BlockByHash(ctx, hash)
|
||||
if err != nil {
|
||||
return crgtypes.BlockTransactionsResponse{}, err
|
||||
}
|
||||
|
||||
txs, err := c.listTransactionsInBlock(ctx, blockResp.Block.Index)
|
||||
if err != nil {
|
||||
return crgtypes.BlockTransactionsResponse{}, err
|
||||
}
|
||||
|
||||
return crgtypes.BlockTransactionsResponse{
|
||||
BlockResponse: blockResp,
|
||||
Transactions: sdkTxsWithHashToRosettaTxs(txs),
|
||||
}, nil
|
||||
return c.blockTxs(ctx, &blockResp.Block.Index)
|
||||
}
|
||||
|
||||
func (c *Client) BlockTransactionsByHeight(ctx context.Context, height *int64) (crgtypes.BlockTransactionsResponse, error) {
|
||||
blockResp, err := c.BlockByHeight(ctx, height)
|
||||
blockTxResp, err := c.blockTxs(ctx, height)
|
||||
if err != nil {
|
||||
return crgtypes.BlockTransactionsResponse{}, err
|
||||
}
|
||||
|
||||
txs, err := c.listTransactionsInBlock(ctx, blockResp.Block.Index)
|
||||
if err != nil {
|
||||
return crgtypes.BlockTransactionsResponse{}, err
|
||||
}
|
||||
|
||||
return crgtypes.BlockTransactionsResponse{
|
||||
BlockResponse: blockResp,
|
||||
Transactions: sdkTxsWithHashToRosettaTxs(txs),
|
||||
}, nil
|
||||
return blockTxResp, nil
|
||||
}
|
||||
|
||||
// Coins fetches the existing coins in the application
|
||||
@ -210,69 +221,80 @@ func (c *Client) coins(ctx context.Context) (sdk.Coins, error) {
|
||||
return supply.Supply, nil
|
||||
}
|
||||
|
||||
// listTransactionsInBlock returns the list of the transactions in a block given its height
|
||||
func (c *Client) listTransactionsInBlock(ctx context.Context, height int64) ([]*sdkTxWithHash, error) {
|
||||
txQuery := fmt.Sprintf(`tx.height=%d`, height)
|
||||
txList, err := c.clientCtx.Client.TxSearch(ctx, txQuery, true, nil, nil, "")
|
||||
if err != nil {
|
||||
return nil, crgerrs.WrapError(crgerrs.ErrUnknown, err.Error())
|
||||
}
|
||||
|
||||
sdkTxs, err := tmResultTxsToSdkTxsWithHash(c.clientCtx.TxConfig.TxDecoder(), txList.Txs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return sdkTxs, nil
|
||||
}
|
||||
|
||||
func (c *Client) TxOperationsAndSignersAccountIdentifiers(signed bool, txBytes []byte) (ops []*types.Operation, signers []*types.AccountIdentifier, err error) {
|
||||
txConfig := c.getTxConfig()
|
||||
rawTx, err := txConfig.TxDecoder()(txBytes)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
txBldr, err := txConfig.WrapTxBuilder(rawTx)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
var accountIdentifierSigners []*types.AccountIdentifier
|
||||
if signed {
|
||||
addrs := txBldr.GetTx().GetSigners()
|
||||
for _, addr := range addrs {
|
||||
signer := &types.AccountIdentifier{
|
||||
Address: addr.String(),
|
||||
}
|
||||
accountIdentifierSigners = append(accountIdentifierSigners, signer)
|
||||
func (c *Client) TxOperationsAndSignersAccountIdentifiers(signed bool, txBytes []byte) (ops []*rosettatypes.Operation, signers []*rosettatypes.AccountIdentifier, err error) {
|
||||
switch signed {
|
||||
case false:
|
||||
rosTx, err := c.converter.ToRosetta().Tx(txBytes, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return rosTx.Operations, nil, err
|
||||
default:
|
||||
ops, signers, err = c.converter.ToRosetta().OpsAndSigners(txBytes)
|
||||
return
|
||||
}
|
||||
|
||||
return sdkTxToOperations(txBldr.GetTx(), false, false), accountIdentifierSigners, nil
|
||||
}
|
||||
|
||||
// GetTx returns a transaction given its hash
|
||||
func (c *Client) GetTx(_ context.Context, hash string) (*types.Transaction, error) {
|
||||
txResp, err := authtx.QueryTx(c.clientCtx, hash)
|
||||
// GetTx returns a transaction given its hash. For Rosetta we make a synthetic transaction for BeginBlock
|
||||
// and EndBlock to adhere to balance tracking rules.
|
||||
func (c *Client) GetTx(ctx context.Context, hash string) (*rosettatypes.Transaction, error) {
|
||||
hashBytes, err := hex.DecodeString(hash)
|
||||
if err != nil {
|
||||
return nil, crgerrs.WrapError(crgerrs.ErrUnknown, err.Error())
|
||||
return nil, crgerrs.WrapError(crgerrs.ErrCodec, fmt.Sprintf("bad tx hash: %s", err))
|
||||
}
|
||||
var sdkTx sdk.Tx
|
||||
err = c.ir.UnpackAny(txResp.Tx, &sdkTx)
|
||||
if err != nil {
|
||||
return nil, crgerrs.WrapError(crgerrs.ErrCodec, err.Error())
|
||||
|
||||
// get tx type and hash
|
||||
txType, hashBytes := c.converter.ToSDK().HashToTxType(hashBytes)
|
||||
|
||||
// construct rosetta tx
|
||||
switch txType {
|
||||
// handle begin block hash
|
||||
case BeginBlockTx:
|
||||
// get block height by hash
|
||||
block, err := c.tmRPC.BlockByHash(ctx, hashBytes)
|
||||
if err != nil {
|
||||
return nil, crgerrs.WrapError(crgerrs.ErrUnknown, err.Error())
|
||||
}
|
||||
|
||||
// get block txs
|
||||
fullBlock, err := c.blockTxs(ctx, &block.Block.Height)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return fullBlock.Transactions[0], nil
|
||||
// handle deliver tx hash
|
||||
case DeliverTxTx:
|
||||
rawTx, err := c.tmRPC.Tx(ctx, hashBytes, true)
|
||||
if err != nil {
|
||||
return nil, crgerrs.WrapError(crgerrs.ErrUnknown, err.Error())
|
||||
}
|
||||
return c.converter.ToRosetta().Tx(rawTx.Tx, &rawTx.TxResult)
|
||||
// handle end block hash
|
||||
case EndBlockTx:
|
||||
// get block height by hash
|
||||
block, err := c.tmRPC.BlockByHash(ctx, hashBytes)
|
||||
if err != nil {
|
||||
return nil, crgerrs.WrapError(crgerrs.ErrUnknown, err.Error())
|
||||
}
|
||||
|
||||
// get block txs
|
||||
fullBlock, err := c.blockTxs(ctx, &block.Block.Height)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// get last tx
|
||||
return fullBlock.Transactions[len(fullBlock.Transactions)-1], nil
|
||||
// unrecognized tx
|
||||
default:
|
||||
return nil, crgerrs.WrapError(crgerrs.ErrBadArgument, fmt.Sprintf("invalid tx hash provided: %s", hash))
|
||||
}
|
||||
return sdkTxWithHashToOperations(&sdkTxWithHash{
|
||||
HexHash: txResp.TxHash,
|
||||
Code: txResp.Code,
|
||||
Log: txResp.RawLog,
|
||||
Tx: sdkTx,
|
||||
}), nil
|
||||
}
|
||||
|
||||
// GetUnconfirmedTx gets an unconfirmed transaction given its hash
|
||||
func (c *Client) GetUnconfirmedTx(ctx context.Context, hash string) (*types.Transaction, error) {
|
||||
res, err := c.clientCtx.Client.UnconfirmedTxs(ctx, nil)
|
||||
func (c *Client) GetUnconfirmedTx(ctx context.Context, hash string) (*rosettatypes.Transaction, error) {
|
||||
res, err := c.tmRPC.UnconfirmedTxs(ctx, nil)
|
||||
if err != nil {
|
||||
return nil, crgerrs.WrapError(crgerrs.ErrNotFound, "unconfirmed tx not found")
|
||||
}
|
||||
@ -282,165 +304,168 @@ func (c *Client) GetUnconfirmedTx(ctx context.Context, hash string) (*types.Tran
|
||||
return nil, crgerrs.WrapError(crgerrs.ErrInterpreting, "invalid hash")
|
||||
}
|
||||
|
||||
for _, tx := range res.Txs {
|
||||
if bytes.Equal(tx.Hash(), hashAsBytes) {
|
||||
sdkTx, err := tmTxToSdkTx(c.clientCtx.TxConfig.TxDecoder(), tx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &types.Transaction{
|
||||
TransactionIdentifier: TmTxToRosettaTxsIdentifier(tx),
|
||||
Operations: sdkTxToOperations(sdkTx, false, false),
|
||||
Metadata: nil,
|
||||
}, nil
|
||||
}
|
||||
// assert that correct tx length is provided
|
||||
switch len(hashAsBytes) {
|
||||
default:
|
||||
return nil, crgerrs.WrapError(crgerrs.ErrBadArgument, fmt.Sprintf("unrecognized tx size: %d", len(hashAsBytes)))
|
||||
case BeginEndBlockTxSize:
|
||||
return nil, crgerrs.WrapError(crgerrs.ErrBadArgument, "endblock and begin block txs cannot be unconfirmed")
|
||||
case DeliverTxSize:
|
||||
break
|
||||
}
|
||||
|
||||
return nil, crgerrs.WrapError(crgerrs.ErrNotFound, "transaction not found in mempool")
|
||||
// iterate over unconfirmed txs to find the one with matching hash
|
||||
for _, unconfirmedTx := range res.Txs {
|
||||
if !bytes.Equal(unconfirmedTx.Hash(), hashAsBytes) {
|
||||
continue
|
||||
}
|
||||
|
||||
return c.converter.ToRosetta().Tx(unconfirmedTx, nil)
|
||||
}
|
||||
return nil, crgerrs.WrapError(crgerrs.ErrNotFound, "transaction not found in mempool: "+hash)
|
||||
}
|
||||
|
||||
// Mempool returns the unconfirmed transactions in the mempool
|
||||
func (c *Client) Mempool(ctx context.Context) ([]*types.TransactionIdentifier, error) {
|
||||
txs, err := c.clientCtx.Client.UnconfirmedTxs(ctx, nil)
|
||||
func (c *Client) Mempool(ctx context.Context) ([]*rosettatypes.TransactionIdentifier, error) {
|
||||
txs, err := c.tmRPC.UnconfirmedTxs(ctx, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return TMTxsToRosettaTxsIdentifiers(txs.Txs), nil
|
||||
return c.converter.ToRosetta().TxIdentifiers(txs.Txs), nil
|
||||
}
|
||||
|
||||
// Peers gets the number of peers
|
||||
func (c *Client) Peers(ctx context.Context) ([]*types.Peer, error) {
|
||||
netInfo, err := c.clientCtx.Client.NetInfo(ctx)
|
||||
func (c *Client) Peers(ctx context.Context) ([]*rosettatypes.Peer, error) {
|
||||
netInfo, err := c.tmRPC.NetInfo(ctx)
|
||||
if err != nil {
|
||||
return nil, crgerrs.WrapError(crgerrs.ErrUnknown, err.Error())
|
||||
}
|
||||
return TmPeersToRosettaPeers(netInfo.Peers), nil
|
||||
return c.converter.ToRosetta().Peers(netInfo.Peers), nil
|
||||
}
|
||||
|
||||
func (c *Client) Status(ctx context.Context) (*types.SyncStatus, error) {
|
||||
status, err := c.clientCtx.Client.Status(ctx)
|
||||
func (c *Client) Status(ctx context.Context) (*rosettatypes.SyncStatus, error) {
|
||||
status, err := c.tmRPC.Status(ctx)
|
||||
if err != nil {
|
||||
return nil, crgerrs.WrapError(crgerrs.ErrUnknown, err.Error())
|
||||
}
|
||||
return TMStatusToRosettaSyncStatus(status), err
|
||||
return c.converter.ToRosetta().SyncStatus(status), err
|
||||
}
|
||||
|
||||
func (c *Client) getTxConfig() client.TxConfig {
|
||||
return c.clientCtx.TxConfig
|
||||
}
|
||||
|
||||
func (c *Client) PostTx(txBytes []byte) (*types.TransactionIdentifier, map[string]interface{}, error) {
|
||||
func (c *Client) PostTx(txBytes []byte) (*rosettatypes.TransactionIdentifier, map[string]interface{}, error) {
|
||||
// sync ensures it will go through checkTx
|
||||
res, err := c.clientCtx.BroadcastTxSync(txBytes)
|
||||
res, err := c.tmRPC.BroadcastTxSync(context.Background(), txBytes)
|
||||
if err != nil {
|
||||
return nil, nil, crgerrs.WrapError(crgerrs.ErrUnknown, err.Error())
|
||||
}
|
||||
// check if tx was broadcast successfully
|
||||
if res.Code != abcitypes.CodeTypeOK {
|
||||
return nil, nil, crgerrs.WrapError(crgerrs.ErrUnknown, fmt.Sprintf("transaction broadcast failure: (%d) %s ", res.Code, res.RawLog))
|
||||
return nil, nil, crgerrs.WrapError(
|
||||
crgerrs.ErrUnknown,
|
||||
fmt.Sprintf("transaction broadcast failure: (%d) %s ", res.Code, res.Log),
|
||||
)
|
||||
}
|
||||
|
||||
return &types.TransactionIdentifier{
|
||||
Hash: res.TxHash,
|
||||
return &rosettatypes.TransactionIdentifier{
|
||||
Hash: fmt.Sprintf("%X", res.Hash),
|
||||
},
|
||||
map[string]interface{}{
|
||||
Log: res.RawLog,
|
||||
Log: res.Log,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// construction endpoints
|
||||
|
||||
// ConstructionMetadataFromOptions builds the metadata given the options
|
||||
func (c *Client) ConstructionMetadataFromOptions(ctx context.Context, options map[string]interface{}) (meta map[string]interface{}, err error) {
|
||||
if len(options) == 0 {
|
||||
return nil, crgerrs.ErrBadArgument
|
||||
}
|
||||
|
||||
addr, ok := options[OptionAddress]
|
||||
if !ok {
|
||||
return nil, crgerrs.WrapError(crgerrs.ErrInvalidAddress, "no address provided")
|
||||
}
|
||||
constructionOptions := new(PreprocessOperationsOptionsResponse)
|
||||
|
||||
addrString, ok := addr.(string)
|
||||
if !ok {
|
||||
return nil, crgerrs.WrapError(crgerrs.ErrInvalidAddress, "address is not a string")
|
||||
}
|
||||
|
||||
accountInfo, err := c.accountInfo(ctx, addrString, nil)
|
||||
err = constructionOptions.FromMetadata(options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
gas, ok := options[OptionGas]
|
||||
if !ok {
|
||||
return nil, crgerrs.WrapError(crgerrs.ErrInvalidAddress, "gas not set")
|
||||
signersData := make([]*SignerData, len(constructionOptions.ExpectedSigners))
|
||||
|
||||
for i, signer := range constructionOptions.ExpectedSigners {
|
||||
accountInfo, err := c.accountInfo(ctx, signer, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
signersData[i] = accountInfo
|
||||
}
|
||||
|
||||
memo, ok := options[OptionMemo]
|
||||
if !ok {
|
||||
return nil, crgerrs.WrapError(crgerrs.ErrInvalidMemo, "memo not set")
|
||||
}
|
||||
|
||||
status, err := c.clientCtx.Client.Status(ctx)
|
||||
status, err := c.tmRPC.Status(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
OptionAccountNumber: accountInfo.GetAccountNumber(),
|
||||
OptionSequence: accountInfo.GetSequence(),
|
||||
OptionChainID: status.NodeInfo.Network,
|
||||
OptionGas: gas,
|
||||
OptionMemo: memo,
|
||||
metadataResp := ConstructionMetadata{
|
||||
ChainID: status.NodeInfo.Network,
|
||||
SignersData: signersData,
|
||||
GasLimit: constructionOptions.GasLimit,
|
||||
GasPrice: constructionOptions.GasPrice,
|
||||
Memo: constructionOptions.Memo,
|
||||
}
|
||||
|
||||
return metadataResp.ToMetadata()
|
||||
}
|
||||
|
||||
func (c *Client) blockTxs(ctx context.Context, height *int64) (crgtypes.BlockTransactionsResponse, error) {
|
||||
// get block info
|
||||
blockInfo, err := c.tmRPC.Block(ctx, height)
|
||||
if err != nil {
|
||||
return crgtypes.BlockTransactionsResponse{}, err
|
||||
}
|
||||
// get block events
|
||||
blockResults, err := c.tmRPC.BlockResults(ctx, height)
|
||||
if err != nil {
|
||||
return crgtypes.BlockTransactionsResponse{}, err
|
||||
}
|
||||
|
||||
if len(blockResults.TxsResults) != len(blockInfo.Block.Txs) {
|
||||
// wtf?
|
||||
panic("block results transactions do now match block transactions")
|
||||
}
|
||||
// process begin and end block txs
|
||||
beginBlockTx := &rosettatypes.Transaction{
|
||||
TransactionIdentifier: &rosettatypes.TransactionIdentifier{Hash: c.converter.ToRosetta().BeginBlockTxHash(blockInfo.BlockID.Hash)},
|
||||
Operations: AddOperationIndexes(
|
||||
nil,
|
||||
c.converter.ToRosetta().BalanceOps(StatusTxSuccess, blockResults.BeginBlockEvents),
|
||||
),
|
||||
}
|
||||
|
||||
endBlockTx := &rosettatypes.Transaction{
|
||||
TransactionIdentifier: &rosettatypes.TransactionIdentifier{Hash: c.converter.ToRosetta().EndBlockTxHash(blockInfo.BlockID.Hash)},
|
||||
Operations: AddOperationIndexes(
|
||||
nil,
|
||||
c.converter.ToRosetta().BalanceOps(StatusTxSuccess, blockResults.EndBlockEvents),
|
||||
),
|
||||
}
|
||||
|
||||
deliverTx := make([]*rosettatypes.Transaction, len(blockInfo.Block.Txs))
|
||||
// process normal txs
|
||||
for i, tx := range blockInfo.Block.Txs {
|
||||
rosTx, err := c.converter.ToRosetta().Tx(tx, blockResults.TxsResults[i])
|
||||
if err != nil {
|
||||
return crgtypes.BlockTransactionsResponse{}, err
|
||||
}
|
||||
deliverTx[i] = rosTx
|
||||
}
|
||||
|
||||
finalTxs := make([]*rosettatypes.Transaction, 0, 2+len(deliverTx))
|
||||
finalTxs = append(finalTxs, beginBlockTx)
|
||||
finalTxs = append(finalTxs, deliverTx...)
|
||||
finalTxs = append(finalTxs, endBlockTx)
|
||||
|
||||
return crgtypes.BlockTransactionsResponse{
|
||||
BlockResponse: c.converter.ToRosetta().BlockResponse(blockInfo),
|
||||
Transactions: finalTxs,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *Client) Ready() error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultNodeTimeout)
|
||||
defer cancel()
|
||||
_, err := c.clientCtx.Client.Health(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = c.bank.TotalSupply(ctx, &bank.QueryTotalSupplyRequest{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) Bootstrap() error {
|
||||
grpcConn, err := grpc.Dial(c.config.GRPCEndpoint, grpc.WithInsecure())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tmRPC, err := http.New(c.config.TendermintRPC, tmWebsocketPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
authClient := auth.NewQueryClient(grpcConn)
|
||||
bankClient := bank.NewQueryClient(grpcConn)
|
||||
|
||||
// NodeURI and Client are set from here otherwise
|
||||
// WitNodeURI will require to create a new client
|
||||
// it's done here because WithNodeURI panics if
|
||||
// connection to tendermint node fails
|
||||
clientCtx := client.Context{
|
||||
Client: tmRPC,
|
||||
NodeURI: c.config.TendermintRPC,
|
||||
}
|
||||
clientCtx = clientCtx.
|
||||
WithJSONMarshaler(c.config.Codec).
|
||||
WithInterfaceRegistry(c.config.InterfaceRegistry).
|
||||
WithTxConfig(authtx.NewTxConfig(c.config.Codec, authtx.DefaultSignModes)).
|
||||
WithAccountRetriever(auth.AccountRetriever{}).
|
||||
WithBroadcastMode(flags.BroadcastBlock)
|
||||
|
||||
c.auth = authClient
|
||||
c.bank = bankClient
|
||||
c.clientCtx = clientCtx
|
||||
c.ir = c.config.InterfaceRegistry
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -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()),
|
||||
}
|
||||
}
|
||||
@ -1,95 +0,0 @@
|
||||
package rosetta
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gogo/protobuf/jsonpb"
|
||||
|
||||
"github.com/coinbase/rosetta-sdk-go/types"
|
||||
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
)
|
||||
|
||||
// opsToMsgsAndFees converts rosetta operations to sdk.Msg and fees represented as sdk.Coins
|
||||
func opsToMsgsAndFees(interfaceRegistry jsonpb.AnyResolver, ops []*types.Operation) ([]sdk.Msg, sdk.Coins, error) {
|
||||
var feeAmnt []*types.Amount
|
||||
var newOps []*types.Operation
|
||||
var msgType string
|
||||
// find the fee operation and put it aside
|
||||
for _, op := range ops {
|
||||
switch op.Type {
|
||||
case OperationFee:
|
||||
amount := op.Amount
|
||||
feeAmnt = append(feeAmnt, amount)
|
||||
default:
|
||||
// check if operation matches the one already used
|
||||
// as, at the moment, we only support operations
|
||||
// that represent a single cosmos-sdk message
|
||||
switch {
|
||||
// if msgType was not set then set it
|
||||
case msgType == "":
|
||||
msgType = op.Type
|
||||
// if msgType does not match op.Type then it means we're trying to send multiple messages in a single tx
|
||||
case msgType != op.Type:
|
||||
return nil, nil, fmt.Errorf("only single message operations are supported: %s - %s", msgType, op.Type)
|
||||
}
|
||||
// append operation to new ops list
|
||||
newOps = append(newOps, op)
|
||||
}
|
||||
}
|
||||
// convert all operations, except fee op to sdk.Msgs
|
||||
msgs, err := opsToMsgs(interfaceRegistry, newOps)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return msgs, amountsToCoins(feeAmnt), nil
|
||||
}
|
||||
|
||||
// amountsToCoins converts rosetta amounts to sdk coins
|
||||
func amountsToCoins(amounts []*types.Amount) sdk.Coins {
|
||||
var feeCoins sdk.Coins
|
||||
|
||||
for _, amount := range amounts {
|
||||
absValue := strings.Trim(amount.Value, "-")
|
||||
value, err := strconv.ParseInt(absValue, 10, 64)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
coin := sdk.NewCoin(amount.Currency.Symbol, sdk.NewInt(value))
|
||||
feeCoins = append(feeCoins, coin)
|
||||
}
|
||||
|
||||
return feeCoins
|
||||
}
|
||||
|
||||
func opsToMsgs(interfaceRegistry jsonpb.AnyResolver, ops []*types.Operation) ([]sdk.Msg, error) {
|
||||
var msgs []sdk.Msg
|
||||
var operationsByType = make(map[string][]*types.Operation)
|
||||
for _, op := range ops {
|
||||
operationsByType[op.Type] = append(operationsByType[op.Type], op)
|
||||
}
|
||||
|
||||
for opName, operations := range operationsByType {
|
||||
if opName == OperationFee {
|
||||
continue
|
||||
}
|
||||
|
||||
msgType, err := interfaceRegistry.Resolve("/" + opName) // Types are registered as /proto-name in the interface registry.
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if rosettaMsg, ok := msgType.(Msg); ok {
|
||||
m, err := rosettaMsg.FromOperations(operations)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
msgs = append(msgs, m)
|
||||
}
|
||||
}
|
||||
|
||||
return msgs, nil
|
||||
}
|
||||
804
server/rosetta/converter.go
Normal file
804
server/rosetta/converter.go
Normal file
@ -0,0 +1,804 @@
|
||||
package rosetta
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
auth "github.com/cosmos/cosmos-sdk/x/auth/types"
|
||||
|
||||
"github.com/tendermint/tendermint/crypto"
|
||||
|
||||
"github.com/btcsuite/btcd/btcec"
|
||||
crgtypes "github.com/tendermint/cosmos-rosetta-gateway/types"
|
||||
tmcoretypes "github.com/tendermint/tendermint/rpc/core/types"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1"
|
||||
"github.com/cosmos/cosmos-sdk/types/tx/signing"
|
||||
|
||||
rosettatypes "github.com/coinbase/rosetta-sdk-go/types"
|
||||
"github.com/gogo/protobuf/proto"
|
||||
crgerrs "github.com/tendermint/cosmos-rosetta-gateway/errors"
|
||||
abci "github.com/tendermint/tendermint/abci/types"
|
||||
tmtypes "github.com/tendermint/tendermint/types"
|
||||
|
||||
sdkclient "github.com/cosmos/cosmos-sdk/client"
|
||||
"github.com/cosmos/cosmos-sdk/codec"
|
||||
codectypes "github.com/cosmos/cosmos-sdk/codec/types"
|
||||
cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types"
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
authsigning "github.com/cosmos/cosmos-sdk/x/auth/signing"
|
||||
banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
|
||||
)
|
||||
|
||||
// Converter is a utility that can be used to convert
|
||||
// back and forth from rosetta to sdk and tendermint types
|
||||
// IMPORTANT NOTES:
|
||||
// - IT SHOULD BE USED ONLY TO DEAL WITH THINGS
|
||||
// IN A STATELESS WAY! IT SHOULD NEVER INTERACT DIRECTLY
|
||||
// WITH TENDERMINT RPC AND COSMOS GRPC
|
||||
//
|
||||
// - IT SHOULD RETURN cosmos rosetta gateway error types!
|
||||
type Converter interface {
|
||||
// ToSDK exposes the methods that convert
|
||||
// rosetta types to cosmos sdk and tendermint types
|
||||
ToSDK() ToSDKConverter
|
||||
// ToRosetta exposes the methods that convert
|
||||
// sdk and tendermint types to rosetta types
|
||||
ToRosetta() ToRosettaConverter
|
||||
}
|
||||
|
||||
// ToRosettaConverter is an interface that exposes
|
||||
// all the functions used to convert sdk and
|
||||
// tendermint types to rosetta known types
|
||||
type ToRosettaConverter interface {
|
||||
// BlockResponse returns a block response given a result block
|
||||
BlockResponse(block *tmcoretypes.ResultBlock) crgtypes.BlockResponse
|
||||
// BeginBlockToTx converts the given begin block hash to rosetta transaction hash
|
||||
BeginBlockTxHash(blockHash []byte) string
|
||||
// EndBlockTxHash converts the given endblock hash to rosetta transaction hash
|
||||
EndBlockTxHash(blockHash []byte) string
|
||||
// Amounts converts sdk.Coins to rosetta.Amounts
|
||||
Amounts(ownedCoins []sdk.Coin, availableCoins sdk.Coins) []*rosettatypes.Amount
|
||||
// Ops converts an sdk.Msg to rosetta operations
|
||||
Ops(status string, msg sdk.Msg) ([]*rosettatypes.Operation, error)
|
||||
// OpsAndSigners takes raw transaction bytes and returns rosetta operations and the expected signers
|
||||
OpsAndSigners(txBytes []byte) (ops []*rosettatypes.Operation, signers []*rosettatypes.AccountIdentifier, err error)
|
||||
// Meta converts an sdk.Msg to rosetta metadata
|
||||
Meta(msg sdk.Msg) (meta map[string]interface{}, err error)
|
||||
// SignerData returns account signing data from a queried any account
|
||||
SignerData(anyAccount *codectypes.Any) (*SignerData, error)
|
||||
// SigningComponents returns rosetta's components required to build a signable transaction
|
||||
SigningComponents(tx authsigning.Tx, metadata *ConstructionMetadata, rosPubKeys []*rosettatypes.PublicKey) (txBytes []byte, payloadsToSign []*rosettatypes.SigningPayload, err error)
|
||||
// Tx converts a tendermint transaction and tx result if provided to a rosetta tx
|
||||
Tx(rawTx tmtypes.Tx, txResult *abci.ResponseDeliverTx) (*rosettatypes.Transaction, error)
|
||||
// TxIdentifiers converts a tendermint tx to transaction identifiers
|
||||
TxIdentifiers(txs []tmtypes.Tx) []*rosettatypes.TransactionIdentifier
|
||||
// BalanceOps converts events to balance operations
|
||||
BalanceOps(status string, events []abci.Event) []*rosettatypes.Operation
|
||||
// SyncStatus converts a tendermint status to sync status
|
||||
SyncStatus(status *tmcoretypes.ResultStatus) *rosettatypes.SyncStatus
|
||||
// Peers converts tendermint peers to rosetta
|
||||
Peers(peers []tmcoretypes.Peer) []*rosettatypes.Peer
|
||||
}
|
||||
|
||||
// ToSDKConverter is an interface that exposes
|
||||
// all the functions used to convert rosetta types
|
||||
// to tendermint and sdk types
|
||||
type ToSDKConverter interface {
|
||||
// UnsignedTx converts rosetta operations to an unsigned cosmos sdk transactions
|
||||
UnsignedTx(ops []*rosettatypes.Operation) (tx authsigning.Tx, err error)
|
||||
// SignedTx adds the provided signatures after decoding the unsigned transaction raw bytes
|
||||
// and returns the signed tx bytes
|
||||
SignedTx(txBytes []byte, signatures []*rosettatypes.Signature) (signedTxBytes []byte, err error)
|
||||
// Msg converts metadata to an sdk message
|
||||
Msg(meta map[string]interface{}, msg sdk.Msg) (err error)
|
||||
// HashToTxType returns the transaction type (end block, begin block or deliver tx)
|
||||
// and the real hash to query in order to get information
|
||||
HashToTxType(hashBytes []byte) (txType TransactionType, realHash []byte)
|
||||
// PubKey attempts to convert a rosetta public key to cosmos sdk one
|
||||
PubKey(pk *rosettatypes.PublicKey) (cryptotypes.PubKey, error)
|
||||
}
|
||||
|
||||
type converter struct {
|
||||
newTxBuilder func() sdkclient.TxBuilder
|
||||
txBuilderFromTx func(tx sdk.Tx) (sdkclient.TxBuilder, error)
|
||||
txDecode sdk.TxDecoder
|
||||
txEncode sdk.TxEncoder
|
||||
bytesToSign func(tx authsigning.Tx, signerData authsigning.SignerData) (b []byte, err error)
|
||||
ir codectypes.InterfaceRegistry
|
||||
cdc *codec.ProtoCodec
|
||||
}
|
||||
|
||||
func NewConverter(cdc *codec.ProtoCodec, ir codectypes.InterfaceRegistry, cfg sdkclient.TxConfig) Converter {
|
||||
return converter{
|
||||
newTxBuilder: cfg.NewTxBuilder,
|
||||
txBuilderFromTx: cfg.WrapTxBuilder,
|
||||
txDecode: cfg.TxDecoder(),
|
||||
txEncode: cfg.TxEncoder(),
|
||||
bytesToSign: func(tx authsigning.Tx, signerData authsigning.SignerData) (b []byte, err error) {
|
||||
bytesToSign, err := cfg.SignModeHandler().GetSignBytes(signing.SignMode_SIGN_MODE_LEGACY_AMINO_JSON, signerData, tx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return crypto.Sha256(bytesToSign), nil
|
||||
},
|
||||
ir: ir,
|
||||
cdc: cdc,
|
||||
}
|
||||
}
|
||||
|
||||
func (c converter) ToSDK() ToSDKConverter {
|
||||
return c
|
||||
}
|
||||
|
||||
func (c converter) ToRosetta() ToRosettaConverter {
|
||||
return c
|
||||
}
|
||||
|
||||
// OpsToUnsignedTx returns all the sdk.Msgs given the operations
|
||||
func (c converter) UnsignedTx(ops []*rosettatypes.Operation) (tx authsigning.Tx, err error) {
|
||||
builder := c.newTxBuilder()
|
||||
|
||||
var msgs []sdk.Msg
|
||||
|
||||
for i := 0; i < len(ops); i++ {
|
||||
op := ops[i]
|
||||
|
||||
protoMessage, err := c.ir.Resolve("/" + op.Type)
|
||||
if err != nil {
|
||||
return nil, crgerrs.WrapError(crgerrs.ErrBadArgument, "operation not found: "+op.Type)
|
||||
}
|
||||
|
||||
msg, ok := protoMessage.(sdk.Msg)
|
||||
if !ok {
|
||||
return nil, crgerrs.WrapError(crgerrs.ErrBadArgument, "operation is not a valid supported sdk.Msg: "+op.Type)
|
||||
}
|
||||
|
||||
err = c.Msg(op.Metadata, msg)
|
||||
if err != nil {
|
||||
return nil, crgerrs.WrapError(crgerrs.ErrCodec, err.Error())
|
||||
}
|
||||
|
||||
// verify message correctness
|
||||
if err = msg.ValidateBasic(); err != nil {
|
||||
return nil, crgerrs.WrapError(
|
||||
crgerrs.ErrBadArgument,
|
||||
fmt.Sprintf("validation of operation at index %d failed: %s", op.OperationIdentifier.Index, err),
|
||||
)
|
||||
}
|
||||
signers := msg.GetSigners()
|
||||
// check if there are enough signers
|
||||
if len(signers) == 0 {
|
||||
return nil, crgerrs.WrapError(crgerrs.ErrBadArgument, fmt.Sprintf("operation at index %d got no signers", op.OperationIdentifier.Index))
|
||||
}
|
||||
// append the msg
|
||||
msgs = append(msgs, msg)
|
||||
// if there's only one signer then simply continue
|
||||
if len(signers) == 1 {
|
||||
continue
|
||||
}
|
||||
// after we have got the msg, we need to verify if the message has multiple signers
|
||||
// if it has got multiple signers, then we need to fetch all the related operations
|
||||
// which involve the other signers of the msg, we expect to find them in order
|
||||
// so if the msg is named "v1.test.Send" and it expects 3 signers, the next 3 operations
|
||||
// must be with the same name "v1.test.Send" and contain the other signers
|
||||
// then we can just skip their processing
|
||||
for j := 0; j < len(signers)-1; j++ {
|
||||
skipOp := ops[i+j] // get the next index
|
||||
// verify that the operation is equal to the new one
|
||||
if skipOp.Type != op.Type {
|
||||
return nil, crgerrs.WrapError(
|
||||
crgerrs.ErrBadArgument,
|
||||
fmt.Sprintf("operation at index %d should have had type %s got: %s", i+j, op.Type, skipOp.Type),
|
||||
)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(op.Metadata, skipOp.Metadata) {
|
||||
return nil, crgerrs.WrapError(
|
||||
crgerrs.ErrBadArgument,
|
||||
fmt.Sprintf("operation at index %d should have had metadata equal to %#v, got: %#v", i+j, op.Metadata, skipOp.Metadata))
|
||||
}
|
||||
|
||||
i++ // increase so we skip it
|
||||
}
|
||||
}
|
||||
|
||||
if err := builder.SetMsgs(msgs...); err != nil {
|
||||
return nil, crgerrs.WrapError(crgerrs.ErrBadArgument, err.Error())
|
||||
}
|
||||
|
||||
return builder.GetTx(), nil
|
||||
|
||||
}
|
||||
|
||||
// Msg unmarshals the rosetta metadata to the given sdk.Msg
|
||||
func (c converter) Msg(meta map[string]interface{}, msg sdk.Msg) error {
|
||||
metaBytes, err := json.Marshal(meta)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.cdc.UnmarshalJSON(metaBytes, msg)
|
||||
}
|
||||
|
||||
func (c converter) Meta(msg sdk.Msg) (meta map[string]interface{}, err error) {
|
||||
b, err := c.cdc.MarshalJSON(msg)
|
||||
if err != nil {
|
||||
return nil, crgerrs.WrapError(crgerrs.ErrCodec, err.Error())
|
||||
}
|
||||
|
||||
err = json.Unmarshal(b, &meta)
|
||||
if err != nil {
|
||||
return nil, crgerrs.WrapError(crgerrs.ErrCodec, err.Error())
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Ops will create an operation for each msg signer
|
||||
// with the message proto name as type, and the raw fields
|
||||
// as metadata
|
||||
func (c converter) Ops(status string, msg sdk.Msg) ([]*rosettatypes.Operation, error) {
|
||||
opName := proto.MessageName(msg)
|
||||
// in case proto does not recognize the message name
|
||||
// then we should try to cast it to service msg, to
|
||||
// check if it was wrapped or not, in case the cast
|
||||
// from sdk.ServiceMsg to sdk.Msg fails, then a
|
||||
// codec error is returned
|
||||
if opName == "" {
|
||||
unwrappedMsg, ok := msg.(sdk.ServiceMsg)
|
||||
if !ok {
|
||||
return nil, crgerrs.WrapError(crgerrs.ErrCodec, fmt.Sprintf("unrecognized message type: %T", msg))
|
||||
}
|
||||
|
||||
msg, ok = unwrappedMsg.Request.(sdk.Msg)
|
||||
if !ok {
|
||||
return nil, crgerrs.WrapError(
|
||||
crgerrs.ErrCodec,
|
||||
fmt.Sprintf("unable to cast %T to sdk.Msg, method: %s", unwrappedMsg.Request, unwrappedMsg.MethodName),
|
||||
)
|
||||
}
|
||||
|
||||
opName = proto.MessageName(msg)
|
||||
if opName == "" {
|
||||
return nil, crgerrs.WrapError(crgerrs.ErrCodec, fmt.Sprintf("unrecognized message type: %T", msg))
|
||||
}
|
||||
}
|
||||
|
||||
meta, err := c.Meta(msg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ops := make([]*rosettatypes.Operation, len(msg.GetSigners()))
|
||||
for i, signer := range msg.GetSigners() {
|
||||
op := &rosettatypes.Operation{
|
||||
Type: opName,
|
||||
Status: status,
|
||||
Account: &rosettatypes.AccountIdentifier{Address: signer.String()},
|
||||
Metadata: meta,
|
||||
}
|
||||
|
||||
ops[i] = op
|
||||
}
|
||||
|
||||
return ops, nil
|
||||
}
|
||||
|
||||
// Tx converts a tendermint raw transaction and its result (if provided) to a rosetta transaction
|
||||
func (c converter) Tx(rawTx tmtypes.Tx, txResult *abci.ResponseDeliverTx) (*rosettatypes.Transaction, error) {
|
||||
// decode tx
|
||||
tx, err := c.txDecode(rawTx)
|
||||
if err != nil {
|
||||
return nil, crgerrs.WrapError(crgerrs.ErrCodec, err.Error())
|
||||
}
|
||||
// get initial status, as per sdk design, if one msg fails
|
||||
// the whole TX will be considered failing, so we can't have
|
||||
// 1 msg being success and 1 msg being reverted
|
||||
status := StatusTxSuccess
|
||||
switch txResult {
|
||||
// if nil, we're probably checking an unconfirmed tx
|
||||
// or trying to build a new transaction, so status
|
||||
// is not put inside
|
||||
case nil:
|
||||
status = ""
|
||||
// set the status
|
||||
default:
|
||||
if txResult.Code != abci.CodeTypeOK {
|
||||
status = StatusTxReverted
|
||||
}
|
||||
}
|
||||
// get operations from msgs
|
||||
msgs := tx.GetMsgs()
|
||||
|
||||
var rawTxOps []*rosettatypes.Operation
|
||||
for _, msg := range msgs {
|
||||
ops, err := c.Ops(status, msg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rawTxOps = append(rawTxOps, ops...)
|
||||
}
|
||||
|
||||
// now get balance events from response deliver tx
|
||||
var balanceOps []*rosettatypes.Operation
|
||||
// tx result might be nil, in case we're querying an unconfirmed tx from the mempool
|
||||
if txResult != nil {
|
||||
balanceOps = c.BalanceOps(status, txResult.Events)
|
||||
}
|
||||
|
||||
// now normalize indexes
|
||||
totalOps := AddOperationIndexes(rawTxOps, balanceOps)
|
||||
|
||||
return &rosettatypes.Transaction{
|
||||
TransactionIdentifier: &rosettatypes.TransactionIdentifier{Hash: fmt.Sprintf("%X", rawTx.Hash())},
|
||||
Operations: totalOps,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c converter) BalanceOps(status string, events []abci.Event) []*rosettatypes.Operation {
|
||||
var ops []*rosettatypes.Operation
|
||||
|
||||
for _, e := range events {
|
||||
balanceOps, ok := sdkEventToBalanceOperations(status, e)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
ops = append(ops, balanceOps...)
|
||||
}
|
||||
|
||||
return ops
|
||||
}
|
||||
|
||||
// sdkEventToBalanceOperations converts an event to a rosetta balance operation
|
||||
// it will panic if the event is malformed because it might mean the sdk spec
|
||||
// has changed and rosetta needs to reflect those changes too.
|
||||
// The balance operations are multiple, one for each denom.
|
||||
func sdkEventToBalanceOperations(status string, event abci.Event) (operations []*rosettatypes.Operation, isBalanceEvent bool) {
|
||||
|
||||
var (
|
||||
accountIdentifier string
|
||||
coinChange sdk.Coins
|
||||
isSub bool
|
||||
)
|
||||
|
||||
switch event.Type {
|
||||
default:
|
||||
return nil, false
|
||||
case banktypes.EventTypeCoinSpent:
|
||||
spender, err := sdk.AccAddressFromBech32((string)(event.Attributes[0].Value))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
coins, err := sdk.ParseCoinsNormalized((string)(event.Attributes[1].Value))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
isSub = true
|
||||
coinChange = coins
|
||||
accountIdentifier = spender.String()
|
||||
|
||||
case banktypes.EventTypeCoinReceived:
|
||||
receiver, err := sdk.AccAddressFromBech32((string)(event.Attributes[0].Value))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
coins, err := sdk.ParseCoinsNormalized((string)(event.Attributes[1].Value))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
isSub = false
|
||||
coinChange = coins
|
||||
accountIdentifier = receiver.String()
|
||||
|
||||
// rosetta does not have the concept of burning coins, so we need to mock
|
||||
// the burn as a send to an address that cannot be resolved to anything
|
||||
case banktypes.EventTypeCoinBurn:
|
||||
coins, err := sdk.ParseCoinsNormalized((string)(event.Attributes[1].Value))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
coinChange = coins
|
||||
accountIdentifier = BurnerAddressIdentifier
|
||||
}
|
||||
|
||||
operations = make([]*rosettatypes.Operation, len(coinChange))
|
||||
|
||||
for i, coin := range coinChange {
|
||||
|
||||
value := coin.Amount.String()
|
||||
// in case the event is a subtract balance one the rewrite value with
|
||||
// the negative coin identifier
|
||||
if isSub {
|
||||
value = "-" + value
|
||||
}
|
||||
|
||||
op := &rosettatypes.Operation{
|
||||
Type: event.Type,
|
||||
Status: status,
|
||||
Account: &rosettatypes.AccountIdentifier{Address: accountIdentifier},
|
||||
Amount: &rosettatypes.Amount{
|
||||
Value: value,
|
||||
Currency: &rosettatypes.Currency{
|
||||
Symbol: coin.Denom,
|
||||
Decimals: 0,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
operations[i] = op
|
||||
}
|
||||
return operations, true
|
||||
}
|
||||
|
||||
// Amounts converts []sdk.Coin to rosetta amounts
|
||||
func (c converter) Amounts(ownedCoins []sdk.Coin, availableCoins sdk.Coins) []*rosettatypes.Amount {
|
||||
amounts := make([]*rosettatypes.Amount, len(availableCoins))
|
||||
ownedCoinsMap := make(map[string]sdk.Int, len(availableCoins))
|
||||
|
||||
for _, ownedCoin := range ownedCoins {
|
||||
ownedCoinsMap[ownedCoin.Denom] = ownedCoin.Amount
|
||||
}
|
||||
|
||||
for i, coin := range availableCoins {
|
||||
value, owned := ownedCoinsMap[coin.Denom]
|
||||
if !owned {
|
||||
amounts[i] = &rosettatypes.Amount{
|
||||
Value: sdk.NewInt(0).String(),
|
||||
Currency: &rosettatypes.Currency{
|
||||
Symbol: coin.Denom,
|
||||
},
|
||||
}
|
||||
continue
|
||||
}
|
||||
amounts[i] = &rosettatypes.Amount{
|
||||
Value: value.String(),
|
||||
Currency: &rosettatypes.Currency{
|
||||
Symbol: coin.Denom,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return amounts
|
||||
}
|
||||
|
||||
// AddOperationIndexes adds the indexes to operations adhering to specific rules:
|
||||
// operations related to messages will be always before than the balance ones
|
||||
func AddOperationIndexes(msgOps []*rosettatypes.Operation, balanceOps []*rosettatypes.Operation) (finalOps []*rosettatypes.Operation) {
|
||||
lenMsgOps := len(msgOps)
|
||||
lenBalanceOps := len(balanceOps)
|
||||
finalOps = make([]*rosettatypes.Operation, 0, lenMsgOps+lenBalanceOps)
|
||||
|
||||
var currentIndex int64
|
||||
// add indexes to msg ops
|
||||
for _, op := range msgOps {
|
||||
op.OperationIdentifier = &rosettatypes.OperationIdentifier{
|
||||
Index: currentIndex,
|
||||
}
|
||||
|
||||
finalOps = append(finalOps, op)
|
||||
currentIndex++
|
||||
}
|
||||
|
||||
// add indexes to balance ops
|
||||
for _, op := range balanceOps {
|
||||
op.OperationIdentifier = &rosettatypes.OperationIdentifier{
|
||||
Index: currentIndex,
|
||||
}
|
||||
|
||||
finalOps = append(finalOps, op)
|
||||
currentIndex++
|
||||
}
|
||||
|
||||
return finalOps
|
||||
}
|
||||
|
||||
// EndBlockTxHash produces a mock endblock hash that rosetta can query
|
||||
// for endblock operations, it also serves the purpose of representing
|
||||
// part of the state changes happening at endblock level (balance ones)
|
||||
func (c converter) EndBlockTxHash(hash []byte) string {
|
||||
final := append([]byte{EndBlockHashStart}, hash...)
|
||||
return fmt.Sprintf("%X", final)
|
||||
}
|
||||
|
||||
// BeginBlockTxHash produces a mock beginblock hash that rosetta can query
|
||||
// for beginblock operations, it also serves the purpose of representing
|
||||
// part of the state changes happening at beginblock level (balance ones)
|
||||
func (c converter) BeginBlockTxHash(hash []byte) string {
|
||||
final := append([]byte{BeginBlockHashStart}, hash...)
|
||||
return fmt.Sprintf("%X", final)
|
||||
}
|
||||
|
||||
// HashToTxType takes the provided hash bytes from rosetta and discerns if they are
|
||||
// a deliver tx type or endblock/begin block hash, returning the real hash afterwards
|
||||
func (c converter) HashToTxType(hashBytes []byte) (txType TransactionType, realHash []byte) {
|
||||
switch len(hashBytes) {
|
||||
case DeliverTxSize:
|
||||
return DeliverTxTx, hashBytes
|
||||
|
||||
case BeginEndBlockTxSize:
|
||||
switch hashBytes[0] {
|
||||
case BeginBlockHashStart:
|
||||
return BeginBlockTx, hashBytes[1:]
|
||||
case EndBlockHashStart:
|
||||
return EndBlockTx, hashBytes[1:]
|
||||
default:
|
||||
return UnrecognizedTx, nil
|
||||
}
|
||||
|
||||
default:
|
||||
return UnrecognizedTx, nil
|
||||
}
|
||||
}
|
||||
|
||||
// StatusToSyncStatus converts a tendermint status to rosetta sync status
|
||||
func (c converter) SyncStatus(status *tmcoretypes.ResultStatus) *rosettatypes.SyncStatus {
|
||||
// determine sync status
|
||||
var stage = StatusPeerSynced
|
||||
if status.SyncInfo.CatchingUp {
|
||||
stage = StatusPeerSyncing
|
||||
}
|
||||
|
||||
return &rosettatypes.SyncStatus{
|
||||
CurrentIndex: status.SyncInfo.LatestBlockHeight,
|
||||
TargetIndex: nil, // sync info does not allow us to get target height
|
||||
Stage: &stage,
|
||||
}
|
||||
}
|
||||
|
||||
// TxIdentifiers converts a tendermint raw transactions into an array of rosetta tx identifiers
|
||||
func (c converter) TxIdentifiers(txs []tmtypes.Tx) []*rosettatypes.TransactionIdentifier {
|
||||
converted := make([]*rosettatypes.TransactionIdentifier, len(txs))
|
||||
for i, tx := range txs {
|
||||
converted[i] = &rosettatypes.TransactionIdentifier{Hash: fmt.Sprintf("%X", tx.Hash())}
|
||||
}
|
||||
|
||||
return converted
|
||||
}
|
||||
|
||||
// tmResultBlockToRosettaBlockResponse converts a tendermint result block to block response
|
||||
func (c converter) BlockResponse(block *tmcoretypes.ResultBlock) crgtypes.BlockResponse {
|
||||
var parentBlock *rosettatypes.BlockIdentifier
|
||||
|
||||
switch block.Block.Height {
|
||||
case 1:
|
||||
parentBlock = &rosettatypes.BlockIdentifier{
|
||||
Index: 1,
|
||||
Hash: fmt.Sprintf("%X", block.BlockID.Hash.Bytes()),
|
||||
}
|
||||
default:
|
||||
parentBlock = &rosettatypes.BlockIdentifier{
|
||||
Index: block.Block.Height - 1,
|
||||
Hash: fmt.Sprintf("%X", block.Block.LastBlockID.Hash.Bytes()),
|
||||
}
|
||||
}
|
||||
return crgtypes.BlockResponse{
|
||||
Block: &rosettatypes.BlockIdentifier{
|
||||
Index: block.Block.Height,
|
||||
Hash: block.Block.Hash().String(),
|
||||
},
|
||||
ParentBlock: parentBlock,
|
||||
MillisecondTimestamp: timeToMilliseconds(block.Block.Time),
|
||||
TxCount: int64(len(block.Block.Txs)),
|
||||
}
|
||||
}
|
||||
|
||||
// Peers converts tm peers to rosetta peers
|
||||
func (c converter) Peers(peers []tmcoretypes.Peer) []*rosettatypes.Peer {
|
||||
converted := make([]*rosettatypes.Peer, len(peers))
|
||||
|
||||
for i, peer := range peers {
|
||||
converted[i] = &rosettatypes.Peer{
|
||||
PeerID: peer.NodeInfo.Moniker,
|
||||
Metadata: map[string]interface{}{
|
||||
"addr": peer.NodeInfo.ListenAddr,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return converted
|
||||
}
|
||||
|
||||
// OpsAndSigners takes transactions bytes and returns the operation, is signed is true it will return
|
||||
// the account identifiers which have signed the transaction
|
||||
func (c converter) OpsAndSigners(txBytes []byte) (ops []*rosettatypes.Operation, signers []*rosettatypes.AccountIdentifier, err error) {
|
||||
|
||||
rosTx, err := c.ToRosetta().Tx(txBytes, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
ops = rosTx.Operations
|
||||
|
||||
// get the signers
|
||||
sdkTx, err := c.txDecode(txBytes)
|
||||
if err != nil {
|
||||
return nil, nil, crgerrs.WrapError(crgerrs.ErrCodec, err.Error())
|
||||
}
|
||||
|
||||
txBuilder, err := c.txBuilderFromTx(sdkTx)
|
||||
if err != nil {
|
||||
return nil, nil, crgerrs.WrapError(crgerrs.ErrCodec, err.Error())
|
||||
}
|
||||
|
||||
for _, signer := range txBuilder.GetTx().GetSigners() {
|
||||
signers = append(signers, &rosettatypes.AccountIdentifier{
|
||||
Address: signer.String(),
|
||||
})
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (c converter) SignedTx(txBytes []byte, signatures []*rosettatypes.Signature) (signedTxBytes []byte, err error) {
|
||||
rawTx, err := c.txDecode(txBytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
txBuilder, err := c.txBuilderFromTx(rawTx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
notSignedSigs, err := txBuilder.GetTx().GetSignaturesV2() //
|
||||
if err != nil {
|
||||
return nil, crgerrs.WrapError(crgerrs.ErrCodec, err.Error())
|
||||
}
|
||||
|
||||
if len(notSignedSigs) != len(signatures) {
|
||||
return nil, crgerrs.WrapError(
|
||||
crgerrs.ErrInvalidTransaction,
|
||||
fmt.Sprintf("expected transaction to have signers data matching the provided signatures: %d <-> %d", len(notSignedSigs), len(signatures)))
|
||||
}
|
||||
|
||||
signedSigs := make([]signing.SignatureV2, len(notSignedSigs))
|
||||
for i, signature := range signatures {
|
||||
// TODO(fdymylja): here we should check that the public key matches...
|
||||
signedSigs[i] = signing.SignatureV2{
|
||||
PubKey: notSignedSigs[i].PubKey,
|
||||
Data: &signing.SingleSignatureData{
|
||||
SignMode: signing.SignMode_SIGN_MODE_LEGACY_AMINO_JSON,
|
||||
Signature: signature.Bytes,
|
||||
},
|
||||
Sequence: notSignedSigs[i].Sequence,
|
||||
}
|
||||
}
|
||||
|
||||
if err = txBuilder.SetSignatures(signedSigs...); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
txBytes, err = c.txEncode(txBuilder.GetTx())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return txBytes, nil
|
||||
}
|
||||
|
||||
func (c converter) PubKey(pubKey *rosettatypes.PublicKey) (cryptotypes.PubKey, error) {
|
||||
if pubKey.CurveType != "secp256k1" {
|
||||
return nil, crgerrs.WrapError(crgerrs.ErrUnsupportedCurve, "only secp256k1 supported")
|
||||
}
|
||||
|
||||
cmp, err := btcec.ParsePubKey(pubKey.Bytes, btcec.S256())
|
||||
if err != nil {
|
||||
return nil, crgerrs.WrapError(crgerrs.ErrBadArgument, err.Error())
|
||||
}
|
||||
|
||||
compressedPublicKey := make([]byte, secp256k1.PubKeySize)
|
||||
copy(compressedPublicKey, cmp.SerializeCompressed())
|
||||
|
||||
pk := &secp256k1.PubKey{Key: compressedPublicKey}
|
||||
|
||||
return pk, nil
|
||||
}
|
||||
|
||||
// SigningComponents takes a sdk tx and construction metadata and returns signable components
|
||||
func (c converter) SigningComponents(tx authsigning.Tx, metadata *ConstructionMetadata, rosPubKeys []*rosettatypes.PublicKey) (txBytes []byte, payloadsToSign []*rosettatypes.SigningPayload, err error) {
|
||||
|
||||
// verify metadata correctness
|
||||
feeAmount, err := sdk.ParseCoinsNormalized(metadata.GasPrice)
|
||||
if err != nil {
|
||||
return nil, nil, crgerrs.WrapError(crgerrs.ErrBadArgument, err.Error())
|
||||
}
|
||||
|
||||
signers := tx.GetSigners()
|
||||
// assert the signers data provided in options are the same as the expected signing accounts
|
||||
// and that the number of rosetta provided public keys equals the one of the signers
|
||||
if len(metadata.SignersData) != len(signers) || len(signers) != len(rosPubKeys) {
|
||||
return nil, nil, crgerrs.WrapError(crgerrs.ErrBadArgument, "signers data and account identifiers mismatch")
|
||||
}
|
||||
|
||||
// add transaction metadata
|
||||
builder, err := c.txBuilderFromTx(tx)
|
||||
if err != nil {
|
||||
return nil, nil, crgerrs.WrapError(crgerrs.ErrCodec, err.Error())
|
||||
}
|
||||
builder.SetFeeAmount(feeAmount)
|
||||
builder.SetGasLimit(metadata.GasLimit)
|
||||
builder.SetMemo(metadata.Memo)
|
||||
|
||||
// build signatures
|
||||
partialSignatures := make([]signing.SignatureV2, len(signers))
|
||||
payloadsToSign = make([]*rosettatypes.SigningPayload, len(signers))
|
||||
|
||||
// pub key ordering matters, in a future release this check might be relaxed
|
||||
for i, signer := range signers {
|
||||
// assert that the provided public keys are correctly ordered
|
||||
// by checking if the signer at index i matches the pubkey at index
|
||||
pubKey, err := c.ToSDK().PubKey(rosPubKeys[0])
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if !bytes.Equal(pubKey.Address().Bytes(), signer.Bytes()) {
|
||||
return nil, nil, crgerrs.WrapError(
|
||||
crgerrs.ErrBadArgument,
|
||||
fmt.Sprintf("public key at index %d does not match the expected transaction signer: %X <-> %X", i, rosPubKeys[i].Bytes, signer.Bytes()),
|
||||
)
|
||||
}
|
||||
|
||||
// set the signer data
|
||||
signerData := authsigning.SignerData{
|
||||
ChainID: metadata.ChainID,
|
||||
AccountNumber: metadata.SignersData[i].AccountNumber,
|
||||
Sequence: metadata.SignersData[i].Sequence,
|
||||
}
|
||||
|
||||
// get signature bytes
|
||||
signBytes, err := c.bytesToSign(tx, signerData)
|
||||
if err != nil {
|
||||
return nil, nil, crgerrs.WrapError(crgerrs.ErrUnknown, fmt.Sprintf("unable to sign tx: %s", err.Error()))
|
||||
}
|
||||
|
||||
// set payload
|
||||
payloadsToSign[i] = &rosettatypes.SigningPayload{
|
||||
AccountIdentifier: &rosettatypes.AccountIdentifier{Address: signer.String()},
|
||||
Bytes: signBytes,
|
||||
SignatureType: rosettatypes.Ecdsa,
|
||||
}
|
||||
|
||||
// set partial signature
|
||||
partialSignatures[i] = signing.SignatureV2{
|
||||
PubKey: pubKey,
|
||||
Data: &signing.SingleSignatureData{}, // needs to be set to empty otherwise the codec will cry
|
||||
Sequence: metadata.SignersData[i].Sequence,
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// now we set the partial signatures in the tx
|
||||
// because we will need to decode the sequence
|
||||
// information of each account in a stateless way
|
||||
err = builder.SetSignatures(partialSignatures...)
|
||||
if err != nil {
|
||||
return nil, nil, crgerrs.WrapError(crgerrs.ErrCodec, err.Error())
|
||||
}
|
||||
|
||||
// finally encode the tx
|
||||
txBytes, err = c.txEncode(builder.GetTx())
|
||||
if err != nil {
|
||||
return nil, nil, crgerrs.WrapError(crgerrs.ErrCodec, err.Error())
|
||||
}
|
||||
|
||||
return txBytes, payloadsToSign, nil
|
||||
}
|
||||
|
||||
// SignerData converts the given any account to signer data
|
||||
func (c converter) SignerData(anyAccount *codectypes.Any) (*SignerData, error) {
|
||||
var acc auth.AccountI
|
||||
err := c.ir.UnpackAny(anyAccount, &acc)
|
||||
if err != nil {
|
||||
return nil, crgerrs.WrapError(crgerrs.ErrCodec, err.Error())
|
||||
}
|
||||
|
||||
return &SignerData{
|
||||
AccountNumber: acc.GetAccountNumber(),
|
||||
Sequence: acc.GetSequence(),
|
||||
}, nil
|
||||
}
|
||||
348
server/rosetta/converter_test.go
Normal file
348
server/rosetta/converter_test.go
Normal file
@ -0,0 +1,348 @@
|
||||
package rosetta_test
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/server/rosetta"
|
||||
|
||||
abci "github.com/tendermint/tendermint/abci/types"
|
||||
|
||||
authsigning "github.com/cosmos/cosmos-sdk/x/auth/signing"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/client"
|
||||
"github.com/cosmos/cosmos-sdk/codec"
|
||||
codectypes "github.com/cosmos/cosmos-sdk/codec/types"
|
||||
|
||||
rosettatypes "github.com/coinbase/rosetta-sdk-go/types"
|
||||
"github.com/stretchr/testify/suite"
|
||||
crgerrs "github.com/tendermint/cosmos-rosetta-gateway/errors"
|
||||
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
authtx "github.com/cosmos/cosmos-sdk/x/auth/tx"
|
||||
bank "github.com/cosmos/cosmos-sdk/x/bank/types"
|
||||
)
|
||||
|
||||
type ConverterTestSuite struct {
|
||||
suite.Suite
|
||||
|
||||
c rosetta.Converter
|
||||
unsignedTxBytes []byte
|
||||
unsignedTx authsigning.Tx
|
||||
|
||||
ir codectypes.InterfaceRegistry
|
||||
cdc *codec.ProtoCodec
|
||||
txConf client.TxConfig
|
||||
}
|
||||
|
||||
func (s *ConverterTestSuite) SetupTest() {
|
||||
// create an unsigned tx
|
||||
const unsignedTxHex = "0a8e010a8b010a1c2f636f736d6f732e62616e6b2e763162657461312e4d736753656e64126b0a2d636f736d6f733134376b6c68377468356a6b6a793361616a736a3272717668747668396d666465333777713567122d636f736d6f73316d6e7670386c786b616679346c787777617175356561653764787630647a36687767797436331a0b0a057374616b651202313612600a4c0a460a1f2f636f736d6f732e63727970746f2e736563703235366b312e5075624b657912230a21034c92046950c876f4a5cb6c7797d6eeb9ef80d67ced4d45fb62b1e859240ba9ad12020a0012100a0a0a057374616b651201311090a10f1a00"
|
||||
unsignedTxBytes, err := hex.DecodeString(unsignedTxHex)
|
||||
s.Require().NoError(err)
|
||||
s.unsignedTxBytes = unsignedTxBytes
|
||||
// instantiate converter
|
||||
cdc, ir := rosetta.MakeCodec()
|
||||
txConfig := authtx.NewTxConfig(cdc, authtx.DefaultSignModes)
|
||||
s.c = rosetta.NewConverter(cdc, ir, txConfig)
|
||||
// add utils
|
||||
s.ir = ir
|
||||
s.cdc = cdc
|
||||
s.txConf = txConfig
|
||||
// add authsigning tx
|
||||
sdkTx, err := txConfig.TxDecoder()(unsignedTxBytes)
|
||||
s.Require().NoError(err)
|
||||
builder, err := txConfig.WrapTxBuilder(sdkTx)
|
||||
s.Require().NoError(err)
|
||||
|
||||
s.unsignedTx = builder.GetTx()
|
||||
}
|
||||
|
||||
func (s *ConverterTestSuite) TestFromRosettaOpsToTxSuccess() {
|
||||
addr1 := sdk.AccAddress("address1").String()
|
||||
addr2 := sdk.AccAddress("address2").String()
|
||||
|
||||
msg1 := &bank.MsgSend{
|
||||
FromAddress: addr1,
|
||||
ToAddress: addr2,
|
||||
Amount: sdk.NewCoins(sdk.NewInt64Coin("test", 10)),
|
||||
}
|
||||
|
||||
msg2 := &bank.MsgSend{
|
||||
FromAddress: addr2,
|
||||
ToAddress: addr1,
|
||||
Amount: sdk.NewCoins(sdk.NewInt64Coin("utxo", 10)),
|
||||
}
|
||||
|
||||
ops, err := s.c.ToRosetta().Ops("", msg1)
|
||||
s.Require().NoError(err)
|
||||
|
||||
ops2, err := s.c.ToRosetta().Ops("", msg2)
|
||||
s.Require().NoError(err)
|
||||
|
||||
ops = append(ops, ops2...)
|
||||
|
||||
tx, err := s.c.ToSDK().UnsignedTx(ops)
|
||||
s.Require().NoError(err)
|
||||
|
||||
getMsgs := tx.GetMsgs()
|
||||
|
||||
s.Require().Equal(2, len(getMsgs))
|
||||
|
||||
s.Require().Equal(getMsgs[0], msg1)
|
||||
s.Require().Equal(getMsgs[1], msg2)
|
||||
|
||||
}
|
||||
|
||||
func (s *ConverterTestSuite) TestFromRosettaOpsToTxErrors() {
|
||||
s.Run("unrecognized op", func() {
|
||||
op := &rosettatypes.Operation{
|
||||
Type: "non-existent",
|
||||
}
|
||||
|
||||
_, err := s.c.ToSDK().UnsignedTx([]*rosettatypes.Operation{op})
|
||||
|
||||
s.Require().ErrorIs(err, crgerrs.ErrBadArgument)
|
||||
})
|
||||
|
||||
s.Run("codec type but not sdk.Msg", func() {
|
||||
op := &rosettatypes.Operation{
|
||||
Type: "cosmos.crypto.ed25519.PubKey",
|
||||
}
|
||||
|
||||
_, err := s.c.ToSDK().UnsignedTx([]*rosettatypes.Operation{op})
|
||||
|
||||
s.Require().ErrorIs(err, crgerrs.ErrBadArgument)
|
||||
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
func (s *ConverterTestSuite) TestMsgToMetaMetaToMsg() {
|
||||
msg := &bank.MsgSend{
|
||||
FromAddress: "addr1",
|
||||
ToAddress: "addr2",
|
||||
Amount: sdk.NewCoins(sdk.NewInt64Coin("test", 10)),
|
||||
}
|
||||
msg.Route()
|
||||
|
||||
meta, err := s.c.ToRosetta().Meta(msg)
|
||||
s.Require().NoError(err)
|
||||
|
||||
copyMsg := new(bank.MsgSend)
|
||||
err = s.c.ToSDK().Msg(meta, copyMsg)
|
||||
s.Require().NoError(err)
|
||||
s.Require().Equal(msg, copyMsg)
|
||||
}
|
||||
|
||||
func (s *ConverterTestSuite) TestSignedTx() {
|
||||
|
||||
s.Run("success", func() {
|
||||
const payloadsJSON = `[{"hex_bytes":"82ccce81a3e4a7272249f0e25c3037a316ee2acce76eb0c25db00ef6634a4d57303b2420edfdb4c9a635ad8851fe5c7a9379b7bc2baadc7d74f7e76ac97459b5","signing_payload":{"address":"cosmos147klh7th5jkjy3aajsj2rqvhtvh9mfde37wq5g","hex_bytes":"ed574d84b095250280de38bf8c254e4a1f8755e5bd300b1f6ca2671688136ecc","account_identifier":{"address":"cosmos147klh7th5jkjy3aajsj2rqvhtvh9mfde37wq5g"},"signature_type":"ecdsa"},"public_key":{"hex_bytes":"034c92046950c876f4a5cb6c7797d6eeb9ef80d67ced4d45fb62b1e859240ba9ad","curve_type":"secp256k1"},"signature_type":"ecdsa"}]`
|
||||
const expectedSignedTxHex = "0a8e010a8b010a1c2f636f736d6f732e62616e6b2e763162657461312e4d736753656e64126b0a2d636f736d6f733134376b6c68377468356a6b6a793361616a736a3272717668747668396d666465333777713567122d636f736d6f73316d6e7670386c786b616679346c787777617175356561653764787630647a36687767797436331a0b0a057374616b651202313612620a4e0a460a1f2f636f736d6f732e63727970746f2e736563703235366b312e5075624b657912230a21034c92046950c876f4a5cb6c7797d6eeb9ef80d67ced4d45fb62b1e859240ba9ad12040a02087f12100a0a0a057374616b651201311090a10f1a4082ccce81a3e4a7272249f0e25c3037a316ee2acce76eb0c25db00ef6634a4d57303b2420edfdb4c9a635ad8851fe5c7a9379b7bc2baadc7d74f7e76ac97459b5"
|
||||
|
||||
var payloads []*rosettatypes.Signature
|
||||
s.Require().NoError(json.Unmarshal([]byte(payloadsJSON), &payloads))
|
||||
|
||||
signedTx, err := s.c.ToSDK().SignedTx(s.unsignedTxBytes, payloads)
|
||||
s.Require().NoError(err)
|
||||
|
||||
signedTxHex := hex.EncodeToString(signedTx)
|
||||
|
||||
s.Require().Equal(signedTxHex, expectedSignedTxHex)
|
||||
})
|
||||
|
||||
s.Run("signers data and signing payloads mismatch", func() {
|
||||
_, err := s.c.ToSDK().SignedTx(s.unsignedTxBytes, nil)
|
||||
s.Require().ErrorIs(err, crgerrs.ErrInvalidTransaction)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *ConverterTestSuite) TestOpsAndSigners() {
|
||||
s.Run("success", func() {
|
||||
addr1 := sdk.AccAddress("address1").String()
|
||||
addr2 := sdk.AccAddress("address2").String()
|
||||
|
||||
msg := &bank.MsgSend{
|
||||
FromAddress: addr1,
|
||||
ToAddress: addr2,
|
||||
Amount: sdk.NewCoins(sdk.NewInt64Coin("test", 10)),
|
||||
}
|
||||
|
||||
builder := s.txConf.NewTxBuilder()
|
||||
s.Require().NoError(builder.SetMsgs(msg))
|
||||
|
||||
sdkTx := builder.GetTx()
|
||||
txBytes, err := s.txConf.TxEncoder()(sdkTx)
|
||||
s.Require().NoError(err)
|
||||
|
||||
ops, signers, err := s.c.ToRosetta().OpsAndSigners(txBytes)
|
||||
s.Require().NoError(err)
|
||||
|
||||
s.Require().Equal(len(ops), len(sdkTx.GetMsgs())*len(sdkTx.GetSigners()), "operation number mismatch")
|
||||
|
||||
s.Require().Equal(len(signers), len(sdkTx.GetSigners()), "signers number mismatch")
|
||||
})
|
||||
}
|
||||
|
||||
func (s *ConverterTestSuite) TestBeginEndBlockAndHashToTxType() {
|
||||
const deliverTxHex = "5229A67AA008B5C5F1A0AEA77D4DEBE146297A30AAEF01777AF10FAD62DD36AB"
|
||||
|
||||
deliverTxBytes, err := hex.DecodeString(deliverTxHex)
|
||||
s.Require().NoError(err)
|
||||
|
||||
endBlockTxHex := s.c.ToRosetta().EndBlockTxHash(deliverTxBytes)
|
||||
beginBlockTxHex := s.c.ToRosetta().BeginBlockTxHash(deliverTxBytes)
|
||||
|
||||
txType, hash := s.c.ToSDK().HashToTxType(deliverTxBytes)
|
||||
|
||||
s.Require().Equal(rosetta.DeliverTxTx, txType)
|
||||
s.Require().Equal(deliverTxBytes, hash, "deliver tx hash should not change")
|
||||
|
||||
endBlockTxBytes, err := hex.DecodeString(endBlockTxHex)
|
||||
s.Require().NoError(err)
|
||||
|
||||
txType, hash = s.c.ToSDK().HashToTxType(endBlockTxBytes)
|
||||
|
||||
s.Require().Equal(rosetta.EndBlockTx, txType)
|
||||
s.Require().Equal(deliverTxBytes, hash, "end block tx hash should be equal to a block hash")
|
||||
|
||||
beginBlockTxBytes, err := hex.DecodeString(beginBlockTxHex)
|
||||
s.Require().NoError(err)
|
||||
|
||||
txType, hash = s.c.ToSDK().HashToTxType(beginBlockTxBytes)
|
||||
|
||||
s.Require().Equal(rosetta.BeginBlockTx, txType)
|
||||
s.Require().Equal(deliverTxBytes, hash, "begin block tx hash should be equal to a block hash")
|
||||
|
||||
txType, hash = s.c.ToSDK().HashToTxType([]byte("invalid"))
|
||||
|
||||
s.Require().Equal(rosetta.UnrecognizedTx, txType)
|
||||
s.Require().Nil(hash)
|
||||
|
||||
txType, hash = s.c.ToSDK().HashToTxType(append([]byte{0x3}, deliverTxBytes...))
|
||||
s.Require().Equal(rosetta.UnrecognizedTx, txType)
|
||||
s.Require().Nil(hash)
|
||||
}
|
||||
|
||||
func (s *ConverterTestSuite) TestSigningComponents() {
|
||||
s.Run("invalid metadata coins", func() {
|
||||
_, _, err := s.c.ToRosetta().SigningComponents(nil, &rosetta.ConstructionMetadata{GasPrice: "invalid"}, nil)
|
||||
s.Require().ErrorIs(err, crgerrs.ErrBadArgument)
|
||||
})
|
||||
|
||||
s.Run("length signers data does not match signers", func() {
|
||||
_, _, err := s.c.ToRosetta().SigningComponents(s.unsignedTx, &rosetta.ConstructionMetadata{GasPrice: "10stake"}, nil)
|
||||
s.Require().ErrorIs(err, crgerrs.ErrBadArgument)
|
||||
})
|
||||
|
||||
s.Run("length pub keys does not match signers", func() {
|
||||
_, _, err := s.c.ToRosetta().SigningComponents(
|
||||
s.unsignedTx,
|
||||
&rosetta.ConstructionMetadata{GasPrice: "10stake", SignersData: []*rosetta.SignerData{
|
||||
{
|
||||
AccountNumber: 0,
|
||||
Sequence: 0,
|
||||
},
|
||||
}},
|
||||
nil)
|
||||
s.Require().ErrorIs(err, crgerrs.ErrBadArgument)
|
||||
})
|
||||
|
||||
s.Run("ros pub key is valid but not the one we expect", func() {
|
||||
validButUnexpected, err := hex.DecodeString("030da9096a40eb1d6c25f1e26e9cbf8941fc84b8f4dc509c8df5e62a29ab8f2415")
|
||||
s.Require().NoError(err)
|
||||
|
||||
_, _, err = s.c.ToRosetta().SigningComponents(
|
||||
s.unsignedTx,
|
||||
&rosetta.ConstructionMetadata{GasPrice: "10stake", SignersData: []*rosetta.SignerData{
|
||||
{
|
||||
AccountNumber: 0,
|
||||
Sequence: 0,
|
||||
},
|
||||
}},
|
||||
[]*rosettatypes.PublicKey{
|
||||
{
|
||||
Bytes: validButUnexpected,
|
||||
CurveType: rosettatypes.Secp256k1,
|
||||
},
|
||||
})
|
||||
s.Require().ErrorIs(err, crgerrs.ErrBadArgument)
|
||||
})
|
||||
|
||||
s.Run("success", func() {
|
||||
expectedPubKey, err := hex.DecodeString("034c92046950c876f4a5cb6c7797d6eeb9ef80d67ced4d45fb62b1e859240ba9ad")
|
||||
s.Require().NoError(err)
|
||||
|
||||
_, _, err = s.c.ToRosetta().SigningComponents(
|
||||
s.unsignedTx,
|
||||
&rosetta.ConstructionMetadata{GasPrice: "10stake", SignersData: []*rosetta.SignerData{
|
||||
{
|
||||
AccountNumber: 0,
|
||||
Sequence: 0,
|
||||
},
|
||||
}},
|
||||
[]*rosettatypes.PublicKey{
|
||||
{
|
||||
Bytes: expectedPubKey,
|
||||
CurveType: rosettatypes.Secp256k1,
|
||||
},
|
||||
})
|
||||
s.Require().NoError(err)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
func (s *ConverterTestSuite) TestBalanceOps() {
|
||||
s.Run("not a balance op", func() {
|
||||
notBalanceOp := abci.Event{
|
||||
Type: "not-a-balance-op",
|
||||
}
|
||||
|
||||
ops := s.c.ToRosetta().BalanceOps("", []abci.Event{notBalanceOp})
|
||||
s.Len(ops, 0, "expected no balance ops")
|
||||
})
|
||||
|
||||
s.Run("multiple balance ops from 2 multicoins event", func() {
|
||||
subBalanceOp := bank.NewCoinSpentEvent(
|
||||
sdk.AccAddress("test"),
|
||||
sdk.NewCoins(sdk.NewInt64Coin("test", 10), sdk.NewInt64Coin("utxo", 10)),
|
||||
)
|
||||
|
||||
addBalanceOp := bank.NewCoinReceivedEvent(
|
||||
sdk.AccAddress("test"),
|
||||
sdk.NewCoins(sdk.NewInt64Coin("test", 10), sdk.NewInt64Coin("utxo", 10)),
|
||||
)
|
||||
|
||||
ops := s.c.ToRosetta().BalanceOps("", []abci.Event{(abci.Event)(subBalanceOp), (abci.Event)(addBalanceOp)})
|
||||
s.Len(ops, 4)
|
||||
})
|
||||
|
||||
s.Run("spec broken", func() {
|
||||
s.Require().Panics(func() {
|
||||
specBrokenSub := abci.Event{
|
||||
Type: bank.EventTypeCoinSpent,
|
||||
}
|
||||
_ = s.c.ToRosetta().BalanceOps("", []abci.Event{specBrokenSub})
|
||||
})
|
||||
|
||||
s.Require().Panics(func() {
|
||||
specBrokenSub := abci.Event{
|
||||
Type: bank.EventTypeCoinBurn,
|
||||
}
|
||||
_ = s.c.ToRosetta().BalanceOps("", []abci.Event{specBrokenSub})
|
||||
})
|
||||
|
||||
s.Require().Panics(func() {
|
||||
specBrokenSub := abci.Event{
|
||||
Type: bank.EventTypeCoinReceived,
|
||||
}
|
||||
_ = s.c.ToRosetta().BalanceOps("", []abci.Event{specBrokenSub})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestConverterTestSuite(t *testing.T) {
|
||||
suite.Run(t, new(ConverterTestSuite))
|
||||
}
|
||||
@ -1,41 +1,104 @@
|
||||
package rosetta
|
||||
|
||||
import (
|
||||
"github.com/coinbase/rosetta-sdk-go/types"
|
||||
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
"crypto/sha256"
|
||||
)
|
||||
|
||||
// statuses
|
||||
const (
|
||||
StatusSuccess = "Success"
|
||||
StatusReverted = "Reverted"
|
||||
StageSynced = "synced"
|
||||
StageSyncing = "syncing"
|
||||
StatusTxSuccess = "Success"
|
||||
StatusTxReverted = "Reverted"
|
||||
StatusPeerSynced = "synced"
|
||||
StatusPeerSyncing = "syncing"
|
||||
)
|
||||
|
||||
// In rosetta all state transitions must be represented as transactions
|
||||
// since in tendermint begin block and end block are state transitions
|
||||
// which are not represented as transactions we mock only the balance changes
|
||||
// happening at those levels as transactions. (check BeginBlockTxHash for more info)
|
||||
const (
|
||||
DeliverTxSize = sha256.Size
|
||||
BeginEndBlockTxSize = DeliverTxSize + 1
|
||||
EndBlockHashStart = 0x0
|
||||
BeginBlockHashStart = 0x1
|
||||
)
|
||||
|
||||
const (
|
||||
// BurnerAddressIdentifier mocks the account identifier of a burner address
|
||||
// all coins burned in the sdk will be sent to this identifier, which per sdk.AccAddress
|
||||
// design we will never be able to query (as of now).
|
||||
// Rosetta does not understand supply contraction.
|
||||
BurnerAddressIdentifier = "burner"
|
||||
)
|
||||
|
||||
// TransactionType is used to distinguish if a rosetta provided hash
|
||||
// represents endblock, beginblock or deliver tx
|
||||
type TransactionType int
|
||||
|
||||
const (
|
||||
UnrecognizedTx TransactionType = iota
|
||||
BeginBlockTx
|
||||
EndBlockTx
|
||||
DeliverTxTx
|
||||
)
|
||||
|
||||
// metadata options
|
||||
|
||||
// misc
|
||||
const (
|
||||
Log = "log"
|
||||
)
|
||||
|
||||
// operations
|
||||
const (
|
||||
OperationFee = "fee"
|
||||
)
|
||||
|
||||
// options
|
||||
const (
|
||||
OptionAccountNumber = "account_number"
|
||||
OptionAddress = "address"
|
||||
OptionChainID = "chain_id"
|
||||
OptionSequence = "sequence"
|
||||
OptionMemo = "memo"
|
||||
OptionGas = "gas"
|
||||
)
|
||||
|
||||
type Msg interface {
|
||||
sdk.Msg
|
||||
ToOperations(withStatus, hasError bool) []*types.Operation
|
||||
FromOperations(ops []*types.Operation) (sdk.Msg, error)
|
||||
// ConstructionPreprocessMetadata is used to represent
|
||||
// the metadata rosetta can provide during preprocess options
|
||||
type ConstructionPreprocessMetadata struct {
|
||||
Memo string `json:"memo"`
|
||||
GasLimit uint64 `json:"gas_limit"`
|
||||
GasPrice string `json:"gas_price"`
|
||||
}
|
||||
|
||||
func (c *ConstructionPreprocessMetadata) FromMetadata(meta map[string]interface{}) error {
|
||||
return unmarshalMetadata(meta, c)
|
||||
}
|
||||
|
||||
// PreprocessOperationsOptionsResponse is the structured metadata options returned by the preprocess operations endpoint
|
||||
type PreprocessOperationsOptionsResponse struct {
|
||||
ExpectedSigners []string `json:"expected_signers"`
|
||||
Memo string `json:"memo"`
|
||||
GasLimit uint64 `json:"gas_limit"`
|
||||
GasPrice string `json:"gas_price"`
|
||||
}
|
||||
|
||||
func (c PreprocessOperationsOptionsResponse) ToMetadata() (map[string]interface{}, error) {
|
||||
return marshalMetadata(c)
|
||||
}
|
||||
|
||||
func (c *PreprocessOperationsOptionsResponse) FromMetadata(meta map[string]interface{}) error {
|
||||
return unmarshalMetadata(meta, c)
|
||||
}
|
||||
|
||||
// SignerData contains information on the signers when the request
|
||||
// is being created, used to populate the account information
|
||||
type SignerData struct {
|
||||
AccountNumber uint64 `json:"account_number"`
|
||||
Sequence uint64 `json:"sequence"`
|
||||
}
|
||||
|
||||
// ConstructionMetadata are the metadata options used to
|
||||
// construct a transaction. It is returned by ConstructionMetadataFromOptions
|
||||
// and fed to ConstructionPayload to process the bytes to sign.
|
||||
type ConstructionMetadata struct {
|
||||
ChainID string `json:"chain_id"`
|
||||
SignersData []*SignerData `json:"signer_data"`
|
||||
GasLimit uint64 `json:"gas_limit"`
|
||||
GasPrice string `json:"gas_price"`
|
||||
Memo string `json:"memo"`
|
||||
}
|
||||
|
||||
func (c ConstructionMetadata) ToMetadata() (map[string]interface{}, error) {
|
||||
return marshalMetadata(c)
|
||||
}
|
||||
|
||||
func (c *ConstructionMetadata) FromMetadata(meta map[string]interface{}) error {
|
||||
return unmarshalMetadata(meta, c)
|
||||
}
|
||||
|
||||
@ -1,112 +1,43 @@
|
||||
package rosetta
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/coinbase/rosetta-sdk-go/types"
|
||||
|
||||
tmcoretypes "github.com/tendermint/tendermint/rpc/core/types"
|
||||
tmtypes "github.com/tendermint/tendermint/types"
|
||||
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
crgerrs "github.com/tendermint/cosmos-rosetta-gateway/errors"
|
||||
)
|
||||
|
||||
// tmResultTxsToSdkTxsWithHash converts tendermint result txs to cosmos sdk.Tx
|
||||
func tmResultTxsToSdkTxsWithHash(decode sdk.TxDecoder, txs []*tmcoretypes.ResultTx) ([]*sdkTxWithHash, error) {
|
||||
converted := make([]*sdkTxWithHash, len(txs))
|
||||
for i, tx := range txs {
|
||||
sdkTx, err := decode(tx.Tx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
converted[i] = &sdkTxWithHash{
|
||||
HexHash: fmt.Sprintf("%X", tx.Tx.Hash()),
|
||||
Code: tx.TxResult.Code,
|
||||
Log: tx.TxResult.Log,
|
||||
Tx: sdkTx,
|
||||
}
|
||||
}
|
||||
|
||||
return converted, nil
|
||||
// timeToMilliseconds converts time to milliseconds timestamp
|
||||
func timeToMilliseconds(t time.Time) int64 {
|
||||
return t.UnixNano() / (int64(time.Millisecond) / int64(time.Nanosecond))
|
||||
}
|
||||
|
||||
func tmTxToSdkTx(decode sdk.TxDecoder, tx tmtypes.Tx) (sdk.Tx, error) {
|
||||
sdkTx, err := decode(tx)
|
||||
// unmarshalMetadata unmarshals the given meta to the target
|
||||
func unmarshalMetadata(meta map[string]interface{}, target interface{}) error {
|
||||
b, err := json.Marshal(meta)
|
||||
if err != nil {
|
||||
return crgerrs.WrapError(crgerrs.ErrCodec, err.Error())
|
||||
}
|
||||
|
||||
err = json.Unmarshal(b, target)
|
||||
if err != nil {
|
||||
return crgerrs.WrapError(crgerrs.ErrCodec, err.Error())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// marshalMetadata marshals the given interface to map[string]interface{}
|
||||
func marshalMetadata(o interface{}) (meta map[string]interface{}, err error) {
|
||||
b, err := json.Marshal(o)
|
||||
if err != nil {
|
||||
return nil, crgerrs.WrapError(crgerrs.ErrCodec, err.Error())
|
||||
}
|
||||
meta = make(map[string]interface{})
|
||||
err = json.Unmarshal(b, &meta)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return sdkTx, err
|
||||
}
|
||||
|
||||
type sdkTxWithHash struct {
|
||||
HexHash string
|
||||
Code uint32
|
||||
Log string
|
||||
Tx sdk.Tx
|
||||
}
|
||||
|
||||
type PayloadReqMetadata struct {
|
||||
ChainID string
|
||||
Sequence uint64
|
||||
AccountNumber uint64
|
||||
Gas uint64
|
||||
Memo string
|
||||
}
|
||||
|
||||
// getMetadataFromPayloadReq obtains the metadata from the request to /construction/payloads endpoint.
|
||||
func getMetadataFromPayloadReq(req *types.ConstructionPayloadsRequest) (*PayloadReqMetadata, error) {
|
||||
chainID, ok := req.Metadata[OptionChainID].(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("chain_id metadata was not provided")
|
||||
}
|
||||
|
||||
sequence, ok := req.Metadata[OptionSequence]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("sequence metadata was not provided")
|
||||
}
|
||||
|
||||
seqNum, ok := sequence.(float64)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid sequence value")
|
||||
}
|
||||
|
||||
accountNum, ok := req.Metadata[OptionAccountNumber]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("account_number metadata was not provided")
|
||||
}
|
||||
|
||||
accNum, ok := accountNum.(float64)
|
||||
if !ok {
|
||||
fmt.Printf("this is type %T", accountNum)
|
||||
return nil, fmt.Errorf("invalid account_number value")
|
||||
}
|
||||
|
||||
gasNum, ok := req.Metadata[OptionGas]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("gas metadata was not provided")
|
||||
}
|
||||
|
||||
gasF64, ok := gasNum.(float64)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid gas value")
|
||||
}
|
||||
|
||||
memo, ok := req.Metadata[OptionMemo]
|
||||
if !ok {
|
||||
memo = ""
|
||||
}
|
||||
|
||||
memoStr, ok := memo.(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid memo")
|
||||
}
|
||||
|
||||
return &PayloadReqMetadata{
|
||||
ChainID: chainID,
|
||||
Sequence: uint64(seqNum),
|
||||
AccountNumber: uint64(accNum),
|
||||
Gas: uint64(gasF64),
|
||||
Memo: memoStr,
|
||||
}, nil
|
||||
return
|
||||
}
|
||||
|
||||
@ -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))
|
||||
}
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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(),
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user