From d4d4da4a6ee762a22c0edef94b4c26d8980c3b88 Mon Sep 17 00:00:00 2001 From: Aleksandr Bezobchuk Date: Tue, 24 Mar 2020 16:36:12 -0400 Subject: [PATCH] Implement client and factory --- client/context/context.go | 1 + client/tx/factory.go | 6 +- client/tx/tx.go | 268 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 272 insertions(+), 3 deletions(-) diff --git a/client/context/context.go b/client/context/context.go index b552bb149a..2970dab6ed 100644 --- a/client/context/context.go +++ b/client/context/context.go @@ -254,6 +254,7 @@ func (ctx CLIContext) PrintOutput(toPrint interface{}) error { out, err = yaml.Marshal(&toPrint) case "json": + // TODO: Use ctx.Marshaler. if ctx.Indent { out, err = ctx.Codec.MarshalJSONIndent(toPrint, "", " ") } else { diff --git a/client/tx/factory.go b/client/tx/factory.go index be1b27e853..a4d3897692 100644 --- a/client/tx/factory.go +++ b/client/tx/factory.go @@ -4,12 +4,12 @@ import ( "io" "strings" - "githuf.com/spf13/viper" - "githuf.com/tendermint/tendermint/crypto" - "githuf.com/cosmos/cosmos-sdk/client/flags" "githuf.com/cosmos/cosmos-sdk/crypto/keys" sdk "githuf.com/cosmos/cosmos-sdk/types" + + "githuf.com/spf13/viper" + "githuf.com/tendermint/tendermint/crypto" ) // AccountRetriever defines the interfaces required for use by the Factory to diff --git a/client/tx/tx.go b/client/tx/tx.go index be25ec7541..49dfb1cddf 100644 --- a/client/tx/tx.go +++ b/client/tx/tx.go @@ -1,7 +1,19 @@ package tx import ( + "bufio" + "errors" + "fmt" + "os" + + "githuf.com/cosmos/cosmos-sdk/client/flags" + "githuf.com/spf13/viper" + + "github.com/cosmos/cosmos-sdk/client/context" + "github.com/cosmos/cosmos-sdk/client/input" + clientkeys "github.com/cosmos/cosmos-sdk/client/keys" "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/crypto/keys" sdk "github.com/cosmos/cosmos-sdk/types" ) @@ -36,3 +48,259 @@ type ( CanonicalSignBytes(cid string, num, seq uint64) ([]byte, error) } ) + +// GenerateOrBroadcastTx will either generate and print and unsigned transaction +// or sign it and broadcast it returning an error upon failure. +func GenerateOrBroadcastTx(ctx context.CLIContext, txf Factory, msgs ...sdk.Msg) error { + if ctx.GenerateOnly { + return GenerateTx(ctx, txf, msgs...) + } + + return BroadcastTx(ctx, txf, msgs...) +} + +// GenerateTx will generate an unsigned transaction and print it to the writer +// specified by ctx.Output. If simulation was requested, the gas will be +// simulated and also printed to the same writer before the transaction is +// printed. +func GenerateTx(ctx context.CLIContext, txf Factory, msgs ...sdk.Msg) error { + if txf.SimulateAndExecute() { + if ctx.Offline { + return errors.New("cannot estimate gas in offline mode") + } + + txBytes, err := BuildSimTx(txf, msgs...) + if err != nil { + return err + } + + _, adjusted, err := CalculateGas(ctx.QueryWithData, txBytes, txf.GasAdjustment()) + if err != nil { + return err + } + + txf = txf.WithGas(adjusted) + _, _ = fmt.Fprintf(os.Stderr, "%s\n", GasEstimateResponse{GasEstimate: txf.Gas()}) + } + + tx, err := BuildUnsignedTx(txf, msgs...) + if err != nil { + return err + } + + out, err := ctx.Marshaler.MarshalJSON(tx) + if err != nil { + return err + } + + _, _ = fmt.Fprintf(ctx.Output, "%s\n", out) + return nil +} + +// BroadcastTx attempts to generate, sign and broadcast a transaction with the +// given set of messages. It will also simulate gas requirements if necessary. +// It will return an error upon failure. +func BroadcastTx(ctx context.CLIContext, txf Factory, msgs ...sdk.Msg) error { + txf, err := PrepareFactory(ctx, txf) + if err != nil { + return err + } + + if txf.SimulateAndExecute() || ctx.Simulate { + txBytes, err := BuildSimTx(txf, msgs...) + if err != nil { + return err + } + + _, adjusted, err := CalculateGas(ctx.QueryWithData, txBytes, txf.GasAdjustment()) + if err != nil { + return err + } + + txf = txf.WithGas(adjusted) + _, _ = fmt.Fprintf(os.Stderr, "%s\n", GasEstimateResponse{GasEstimate: txf.Gas()}) + } + + if ctx.Simulate { + return nil + } + + tx, err := BuildUnsignedTx(txf, msgs...) + if err != nil { + return err + } + + if !ctx.SkipConfirm { + out, err := ctx.Marshaler.MarshalJSON(tx) + if err != nil { + return err + } + + _, _ = fmt.Fprintf(os.Stderr, "%s\n\n", out) + + 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 + } + } + + txBytes, err := Sign(txf, ctx.GetFromName(), clientkeys.DefaultKeyPass, tx) + if err != nil { + return err + } + + // broadcast to a Tendermint node + res, err := ctx.BroadcastTx(txBytes) + if err != nil { + return err + } + + return ctx.PrintOutput(res) +} + +// BuildUnsignedTx builds a transaction to be signed given a set of messages. The +// transaction is initially created via the provided factory's generator. Once +// created, the fee, memo, and messages are set. +func BuildUnsignedTx(txf Factory, msgs ...sdk.Msg) (ClientTx, error) { + if txf.chainID == "" { + return nil, fmt.Errorf("chain ID required but not specified") + } + + fees := txf.fees + if !txf.gasPrices.IsZero() { + if !fees.IsZero() { + return nil, errors.New("cannot provide both fees and gas prices") + } + + glDec := sdk.NewDec(int64(txf.gas)) + + // Derive the fees based on the provided gas prices, where + // fee = ceil(gasPrice * gasLimit). + fees = make(sdk.Coins, len(txf.gasPrices)) + for i, gp := range txf.gasPrices { + fee := gp.Amount.Mul(glDec) + fees[i] = sdk.NewCoin(gp.Denom, fee.Ceil().RoundInt()) + } + } + + tx := txf.txGenerator.NewTx() + tx.SetFee(txf.feeFn(txf.gas, fees)) + tx.SetMsgs(msgs...) + tx.SetMemo(txf.memo) + tx.SetSignatures(nil) + + return tx, nil +} + +// BuildSimTx creates an unsigned tx with an empty single signature and returns +// the encoded transaction or an error if the unsigned transaction cannot be +// built. +func BuildSimTx(txf Factory, msgs ...sdk.Msg) ([]byte, error) { + tx, err := BuildUnsignedTx(txf, msgs...) + if err != nil { + return nil, err + } + + // Create an empty signature literal as the ante handler will populate with a + // sentinel pubkey. + tx.SetSignatures(txf.sigFn(nil, nil)) + + return tx.Marshal() +} + +// CalculateGas simulates the execution of a transaction and returns the +// simulation response obtained by the query and the adjusted gas amount. +func CalculateGas( + queryFunc func(string, []byte) ([]byte, int64, error), txBytes []byte, adjustment float64, +) (sdk.SimulationResponse, uint64, error) { + + rawRes, _, err := queryFunc("/app/simulate", txBytes) + if err != nil { + return sdk.SimulationResponse{}, 0, err + } + + // TODO: Use JSON or proto instead of codec.cdc + var simRes sdk.SimulationResponse + if err := codec.Cdc.UnmarshalBinaryBare(rawRes, &simRes); err != nil { + return sdk.SimulationResponse{}, 0, err + } + + return simRes, uint64(adjustment * float64(simRes.GasUsed)), nil +} + +// PrepareFactory ensures the account defined by ctx.GetFromAddress() exists and +// if the account number and/or the account sequence number are zero (not set), +// they will be queried for and set on the provided Factory. A new Factory with +// the updated fields will be returned. +func PrepareFactory(ctx context.CLIContext, txf Factory) (Factory, error) { + from := ctx.GetFromAddress() + + if err := txf.accountRetriever.EnsureExists(from); err != nil { + return txf, err + } + + initNum, initSeq := txf.accountNumber, txf.sequence + if initNum == 0 || initSeq == 0 { + num, seq, err := txf.accountRetriever.GetAccountNumberSequence(from) + if err != nil { + return txf, err + } + + if initNum == 0 { + txf = txf.WithAccountNumber(num) + } + if initSeq == 0 { + txf = txf.WithSequence(seq) + } + } + + return txf, nil +} + +// Sign signs a given tx with the provided name and passphrase. If the Factory's +// Keybase is not set, a new one will be created based on the client's backend. +// The bytes signed over are canconical. The resulting signature will be set on +// the transaction. Finally, the marshaled transaction is returned. An error is +// returned upon failure. +// +// Note, It is assumed the Factory has the necessary fields set that are required +// by the CanonicalSignBytes call. +func Sign(txf Factory, name, passphrase string, tx ClientTx) ([]byte, error) { + if txf.keybase == nil { + keybase, err := keys.NewKeyring( + sdk.KeyringServiceName(), + viper.GetString(flags.FlagKeyringBackend), + viper.GetString(flags.FlagHome), + os.Stdin, + ) + if err != nil { + return nil, err + } + + txf = txf.WithKeybase(keybase) + } + + signBytes, err := tx.CanonicalSignBytes(txf.chainID, txf.accountNumber, txf.sequence) + if err != nil { + return nil, err + } + + sigBytes, pubkey, err := txf.keybase.Sign(name, passphrase, signBytes) + if err != nil { + return nil, err + } + + tx.SetSignatures(txf.sigFn(pubkey, sigBytes)) + return tx.Marshal() +} + +// 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) +}