Packages named utils, common, or misc provide clients with no sense of what the package contains. This makes it harder for clients to use the package and makes it harder for maintainers to keep the package focused. Over time, they accumulate dependencies that can make compilation significantly and unnecessarily slower, especially in large programs. And since such package names are generic, they are more likely to collide with other packages imported by client code, forcing clients to invent names to distinguish them. cit. https://blog.golang.org/package-names
349 lines
9.8 KiB
Go
349 lines
9.8 KiB
Go
package client
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"os"
|
|
|
|
"github.com/pkg/errors"
|
|
"github.com/spf13/viper"
|
|
|
|
"github.com/cosmos/cosmos-sdk/client/context"
|
|
"github.com/cosmos/cosmos-sdk/client/flags"
|
|
"github.com/cosmos/cosmos-sdk/client/input"
|
|
"github.com/cosmos/cosmos-sdk/client/keys"
|
|
"github.com/cosmos/cosmos-sdk/codec"
|
|
sdk "github.com/cosmos/cosmos-sdk/types"
|
|
authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
|
|
)
|
|
|
|
// GasEstimateResponse defines a response definition for tx gas estimation.
|
|
type GasEstimateResponse struct {
|
|
GasEstimate uint64 `json:"gas_estimate" yaml:"gas_estimate"`
|
|
}
|
|
|
|
func (gr GasEstimateResponse) String() string {
|
|
return fmt.Sprintf("gas estimate: %d", gr.GasEstimate)
|
|
}
|
|
|
|
// GenerateOrBroadcastMsgs creates a StdTx given a series of messages. If
|
|
// the provided context has generate-only enabled, the tx will only be printed
|
|
// to STDOUT in a fully offline manner. Otherwise, the tx will be signed and
|
|
// broadcasted.
|
|
func GenerateOrBroadcastMsgs(cliCtx context.CLIContext, txBldr authtypes.TxBuilder, msgs []sdk.Msg) error {
|
|
if cliCtx.GenerateOnly {
|
|
return PrintUnsignedStdTx(txBldr, cliCtx, msgs)
|
|
}
|
|
|
|
return CompleteAndBroadcastTxCLI(txBldr, cliCtx, msgs)
|
|
}
|
|
|
|
// CompleteAndBroadcastTxCLI implements a utility function that facilitates
|
|
// sending a series of messages in a signed transaction given a TxBuilder and a
|
|
// QueryContext. It ensures that the account exists, has a proper number and
|
|
// sequence set. In addition, it builds and signs a transaction with the
|
|
// supplied messages. Finally, it broadcasts the signed transaction to a node.
|
|
func CompleteAndBroadcastTxCLI(txBldr authtypes.TxBuilder, cliCtx context.CLIContext, msgs []sdk.Msg) error {
|
|
txBldr, err := PrepareTxBuilder(txBldr, cliCtx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
fromName := cliCtx.GetFromName()
|
|
|
|
if txBldr.SimulateAndExecute() || cliCtx.Simulate {
|
|
txBldr, err = EnrichWithGas(txBldr, cliCtx, msgs)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
gasEst := GasEstimateResponse{GasEstimate: txBldr.Gas()}
|
|
_, _ = fmt.Fprintf(os.Stderr, "%s\n", gasEst.String())
|
|
}
|
|
|
|
if cliCtx.Simulate {
|
|
return nil
|
|
}
|
|
|
|
if !cliCtx.SkipConfirm {
|
|
stdSignMsg, err := txBldr.BuildSignMsg(msgs)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var json []byte
|
|
if viper.GetBool(flags.FlagIndentResponse) {
|
|
json, err = cliCtx.Codec.MarshalJSONIndent(stdSignMsg, "", " ")
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
} else {
|
|
json = cliCtx.Codec.MustMarshalJSON(stdSignMsg)
|
|
}
|
|
|
|
_, _ = fmt.Fprintf(os.Stderr, "%s\n\n", json)
|
|
|
|
buf := bufio.NewReader(os.Stdin)
|
|
ok, err := input.GetConfirmation("confirm transaction before signing and broadcasting", buf)
|
|
if err != nil || !ok {
|
|
_, _ = fmt.Fprintf(os.Stderr, "%s\n", "cancelled transaction")
|
|
return err
|
|
}
|
|
}
|
|
|
|
// build and sign the transaction
|
|
txBytes, err := txBldr.BuildAndSign(fromName, keys.DefaultKeyPass, msgs)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// broadcast to a Tendermint node
|
|
res, err := cliCtx.BroadcastTx(txBytes)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return cliCtx.PrintOutput(res)
|
|
}
|
|
|
|
// EnrichWithGas calculates the gas estimate that would be consumed by the
|
|
// transaction and set the transaction's respective value accordingly.
|
|
func EnrichWithGas(txBldr authtypes.TxBuilder, cliCtx context.CLIContext, msgs []sdk.Msg) (authtypes.TxBuilder, error) {
|
|
_, adjusted, err := simulateMsgs(txBldr, cliCtx, msgs)
|
|
if err != nil {
|
|
return txBldr, err
|
|
}
|
|
|
|
return txBldr.WithGas(adjusted), nil
|
|
}
|
|
|
|
// CalculateGas simulates the execution of a transaction and returns
|
|
// both the estimate obtained by the query and the adjusted amount.
|
|
func CalculateGas(
|
|
queryFunc func(string, []byte) ([]byte, int64, error), cdc *codec.Codec,
|
|
txBytes []byte, adjustment float64,
|
|
) (estimate, adjusted uint64, err error) {
|
|
|
|
// run a simulation (via /app/simulate query) to
|
|
// estimate gas and update TxBuilder accordingly
|
|
rawRes, _, err := queryFunc("/app/simulate", txBytes)
|
|
if err != nil {
|
|
return estimate, adjusted, err
|
|
}
|
|
|
|
estimate, err = parseQueryResponse(cdc, rawRes)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
adjusted = adjustGasEstimate(estimate, adjustment)
|
|
return estimate, adjusted, nil
|
|
}
|
|
|
|
// PrintUnsignedStdTx builds an unsigned StdTx and prints it to os.Stdout.
|
|
func PrintUnsignedStdTx(txBldr authtypes.TxBuilder, cliCtx context.CLIContext, msgs []sdk.Msg) error {
|
|
stdTx, err := buildUnsignedStdTxOffline(txBldr, cliCtx, msgs)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var json []byte
|
|
if viper.GetBool(flags.FlagIndentResponse) {
|
|
json, err = cliCtx.Codec.MarshalJSONIndent(stdTx, "", " ")
|
|
} else {
|
|
json, err = cliCtx.Codec.MarshalJSON(stdTx)
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
_, _ = fmt.Fprintf(cliCtx.Output, "%s\n", json)
|
|
return nil
|
|
}
|
|
|
|
// SignStdTx appends a signature to a StdTx and returns a copy of it. If appendSig
|
|
// is false, it replaces the signatures already attached with the new signature.
|
|
// Don't perform online validation or lookups if offline is true.
|
|
func SignStdTx(
|
|
txBldr authtypes.TxBuilder, cliCtx context.CLIContext, name string,
|
|
stdTx authtypes.StdTx, appendSig bool, offline bool,
|
|
) (authtypes.StdTx, error) {
|
|
|
|
var signedStdTx authtypes.StdTx
|
|
|
|
info, err := txBldr.Keybase().Get(name)
|
|
if err != nil {
|
|
return signedStdTx, err
|
|
}
|
|
|
|
addr := info.GetPubKey().Address()
|
|
|
|
// check whether the address is a signer
|
|
if !isTxSigner(sdk.AccAddress(addr), stdTx.GetSigners()) {
|
|
return signedStdTx, fmt.Errorf("%s: %s", authtypes.ErrorInvalidSigner, name)
|
|
}
|
|
|
|
if !offline {
|
|
txBldr, err = populateAccountFromState(txBldr, cliCtx, sdk.AccAddress(addr))
|
|
if err != nil {
|
|
return signedStdTx, err
|
|
}
|
|
}
|
|
|
|
return txBldr.SignStdTx(name, keys.DefaultKeyPass, stdTx, appendSig)
|
|
}
|
|
|
|
// SignStdTxWithSignerAddress attaches a signature to a StdTx and returns a copy of a it.
|
|
// Don't perform online validation or lookups if offline is true, else
|
|
// populate account and sequence numbers from a foreign account.
|
|
func SignStdTxWithSignerAddress(txBldr authtypes.TxBuilder, cliCtx context.CLIContext,
|
|
addr sdk.AccAddress, name string, stdTx authtypes.StdTx,
|
|
offline bool) (signedStdTx authtypes.StdTx, err error) {
|
|
|
|
// check whether the address is a signer
|
|
if !isTxSigner(addr, stdTx.GetSigners()) {
|
|
return signedStdTx, fmt.Errorf("%s: %s", authtypes.ErrorInvalidSigner, name)
|
|
}
|
|
|
|
if !offline {
|
|
txBldr, err = populateAccountFromState(txBldr, cliCtx, addr)
|
|
if err != nil {
|
|
return signedStdTx, err
|
|
}
|
|
}
|
|
|
|
return txBldr.SignStdTx(name, keys.DefaultKeyPass, stdTx, false)
|
|
}
|
|
|
|
// Read and decode a StdTx from the given filename. Can pass "-" to read from stdin.
|
|
func ReadStdTxFromFile(cdc *codec.Codec, filename string) (stdTx authtypes.StdTx, err error) {
|
|
var bytes []byte
|
|
|
|
if filename == "-" {
|
|
bytes, err = ioutil.ReadAll(os.Stdin)
|
|
} else {
|
|
bytes, err = ioutil.ReadFile(filename)
|
|
}
|
|
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
if err = cdc.UnmarshalJSON(bytes, &stdTx); err != nil {
|
|
return
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
func populateAccountFromState(
|
|
txBldr authtypes.TxBuilder, cliCtx context.CLIContext, addr sdk.AccAddress,
|
|
) (authtypes.TxBuilder, error) {
|
|
|
|
num, seq, err := authtypes.NewAccountRetriever(cliCtx).GetAccountNumberSequence(addr)
|
|
if err != nil {
|
|
return txBldr, err
|
|
}
|
|
|
|
return txBldr.WithAccountNumber(num).WithSequence(seq), nil
|
|
}
|
|
|
|
// GetTxEncoder return tx encoder from global sdk configuration if ones is defined.
|
|
// Otherwise returns encoder with default logic.
|
|
func GetTxEncoder(cdc *codec.Codec) (encoder sdk.TxEncoder) {
|
|
encoder = sdk.GetConfig().GetTxEncoder()
|
|
if encoder == nil {
|
|
encoder = authtypes.DefaultTxEncoder(cdc)
|
|
}
|
|
|
|
return encoder
|
|
}
|
|
|
|
// nolint
|
|
// SimulateMsgs simulates the transaction and returns the gas estimate and the adjusted value.
|
|
func simulateMsgs(txBldr authtypes.TxBuilder, cliCtx context.CLIContext, msgs []sdk.Msg) (estimated, adjusted uint64, err error) {
|
|
txBytes, err := txBldr.BuildTxForSim(msgs)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
estimated, adjusted, err = CalculateGas(cliCtx.QueryWithData, cliCtx.Codec, txBytes, txBldr.GasAdjustment())
|
|
return
|
|
}
|
|
|
|
func adjustGasEstimate(estimate uint64, adjustment float64) uint64 {
|
|
return uint64(adjustment * float64(estimate))
|
|
}
|
|
|
|
func parseQueryResponse(cdc *codec.Codec, rawRes []byte) (uint64, error) {
|
|
var gasUsed uint64
|
|
if err := cdc.UnmarshalBinaryLengthPrefixed(rawRes, &gasUsed); err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
return gasUsed, nil
|
|
}
|
|
|
|
// PrepareTxBuilder populates a TxBuilder in preparation for the build of a Tx.
|
|
func PrepareTxBuilder(txBldr authtypes.TxBuilder, cliCtx context.CLIContext) (authtypes.TxBuilder, error) {
|
|
from := cliCtx.GetFromAddress()
|
|
|
|
accGetter := authtypes.NewAccountRetriever(cliCtx)
|
|
if err := accGetter.EnsureExists(from); err != nil {
|
|
return txBldr, err
|
|
}
|
|
|
|
txbldrAccNum, txbldrAccSeq := txBldr.AccountNumber(), txBldr.Sequence()
|
|
// TODO: (ref #1903) Allow for user supplied account number without
|
|
// automatically doing a manual lookup.
|
|
if txbldrAccNum == 0 || txbldrAccSeq == 0 {
|
|
num, seq, err := authtypes.NewAccountRetriever(cliCtx).GetAccountNumberSequence(from)
|
|
if err != nil {
|
|
return txBldr, err
|
|
}
|
|
|
|
if txbldrAccNum == 0 {
|
|
txBldr = txBldr.WithAccountNumber(num)
|
|
}
|
|
if txbldrAccSeq == 0 {
|
|
txBldr = txBldr.WithSequence(seq)
|
|
}
|
|
}
|
|
|
|
return txBldr, nil
|
|
}
|
|
|
|
func buildUnsignedStdTxOffline(txBldr authtypes.TxBuilder, cliCtx context.CLIContext, msgs []sdk.Msg) (stdTx authtypes.StdTx, err error) {
|
|
if txBldr.SimulateAndExecute() {
|
|
if cliCtx.GenerateOnly {
|
|
return stdTx, errors.New("cannot estimate gas with generate-only")
|
|
}
|
|
|
|
txBldr, err = EnrichWithGas(txBldr, cliCtx, msgs)
|
|
if err != nil {
|
|
return stdTx, err
|
|
}
|
|
|
|
_, _ = fmt.Fprintf(os.Stderr, "estimated gas = %v\n", txBldr.Gas())
|
|
}
|
|
|
|
stdSignMsg, err := txBldr.BuildSignMsg(msgs)
|
|
if err != nil {
|
|
return stdTx, err
|
|
}
|
|
|
|
return authtypes.NewStdTx(stdSignMsg.Msgs, stdSignMsg.Fee, nil, stdSignMsg.Memo), nil
|
|
}
|
|
|
|
func isTxSigner(user sdk.AccAddress, signers []sdk.AccAddress) bool {
|
|
for _, s := range signers {
|
|
if bytes.Equal(user.Bytes(), s.Bytes()) {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|