From 2b43e25d551f16d5b9d504f52f61ef5f9df9866f Mon Sep 17 00:00:00 2001 From: Alexander Bezobchuk Date: Mon, 25 Mar 2019 20:54:23 -0400 Subject: [PATCH] Merge PR #3954: Tx Broadcasting Sync by Default --- ...th---broadcast-mode-flag-where-the-default | 3 + baseapp/baseapp.go | 12 ++-- client/config.go | 36 ++++++----- client/context/broadcast.go | 60 ++++++++++--------- client/context/context.go | 11 +++- client/flags.go | 15 ++++- client/lcd/swagger-ui/swagger.yaml | 4 +- client/lcd/test_helpers.go | 2 +- client/tx/broadcast.go | 31 ++-------- client/utils/utils.go | 8 ++- cmd/gaia/cli_test/cli_test.go | 4 +- cmd/gaia/cli_test/test_helpers.go | 2 + docs/gaia/gaiacli.md | 13 ++++ types/result.go | 47 ++++++++++++++- 14 files changed, 158 insertions(+), 90 deletions(-) create mode 100644 .pending/breaking/gaia/3875-Replace-async-flag-with---broadcast-mode-flag-where-the-default diff --git a/.pending/breaking/gaia/3875-Replace-async-flag-with---broadcast-mode-flag-where-the-default b/.pending/breaking/gaia/3875-Replace-async-flag-with---broadcast-mode-flag-where-the-default new file mode 100644 index 0000000000..dbbf6928aa --- /dev/null +++ b/.pending/breaking/gaia/3875-Replace-async-flag-with---broadcast-mode-flag-where-the-default @@ -0,0 +1,3 @@ +#3875 Replace `async` flag with `--broadcast-mode` flag where the default +value is `sync`. The `block` mode should not be used. The REST client now +uses `mode` parameter instead of the `return` parameter. diff --git a/baseapp/baseapp.go b/baseapp/baseapp.go index 8b30f4a739..dc51f541da 100644 --- a/baseapp/baseapp.go +++ b/baseapp/baseapp.go @@ -661,7 +661,7 @@ func (app *BaseApp) getContextForTx(mode runTxMode, txBytes []byte) (ctx sdk.Con // runMsgs iterates through all the messages and executes them. func (app *BaseApp) runMsgs(ctx sdk.Context, msgs []sdk.Msg, mode runTxMode) (result sdk.Result) { - idxlogs := make([]sdk.ABCIMessageLog, 0, len(msgs)) // a list of JSON-encoded logs with msg index + idxLogs := make([]sdk.ABCIMessageLog, 0, len(msgs)) // a list of JSON-encoded logs with msg index var data []byte // NOTE: we just append them all (?!) var tags sdk.Tags // also just append them all @@ -695,7 +695,7 @@ func (app *BaseApp) runMsgs(ctx sdk.Context, msgs []sdk.Msg, mode runTxMode) (re // stop execution and return on first failed message if !msgResult.IsOK() { idxLog.Success = false - idxlogs = append(idxlogs, idxLog) + idxLogs = append(idxLogs, idxLog) code = msgResult.Code codespace = msgResult.Codespace @@ -703,10 +703,10 @@ func (app *BaseApp) runMsgs(ctx sdk.Context, msgs []sdk.Msg, mode runTxMode) (re } idxLog.Success = true - idxlogs = append(idxlogs, idxLog) + idxLogs = append(idxLogs, idxLog) } - logJSON := codec.Cdc.MustMarshalJSON(idxlogs) + logJSON := codec.Cdc.MustMarshalJSON(idxLogs) result = sdk.Result{ Code: code, Codespace: codespace, @@ -719,7 +719,7 @@ func (app *BaseApp) runMsgs(ctx sdk.Context, msgs []sdk.Msg, mode runTxMode) (re return result } -// Returns the applicantion's deliverState if app is in runTxModeDeliver, +// Returns the applications's deliverState if app is in runTxModeDeliver, // otherwise it returns the application's checkstate. func (app *BaseApp) getState(mode runTxMode) *state { if mode == runTxModeCheck || mode == runTxModeSimulate { @@ -829,7 +829,7 @@ func (app *BaseApp) runTx(mode runTxMode, txBytes []byte, tx sdk.Tx) (result sdk // performance benefits, but it'll be more difficult to get right. anteCtx, msCache = app.cacheTxContext(ctx, txBytes) - newCtx, result, abort := app.anteHandler(anteCtx, tx, (mode == runTxModeSimulate)) + newCtx, result, abort := app.anteHandler(anteCtx, tx, mode == runTxModeSimulate) if !newCtx.IsZero() { // At this point, newCtx.MultiStore() is cache-wrapped, or something else // replaced by the ante handler. We want the original multistore, not one diff --git a/client/config.go b/client/config.go index 2bbc7c79e5..d785093a30 100644 --- a/client/config.go +++ b/client/config.go @@ -18,14 +18,11 @@ const ( flagGet = "get" ) -var configDefaults map[string]string - -func init() { - configDefaults = map[string]string{ - "chain-id": "", - "output": "text", - "node": "tcp://localhost:26657", - } +var configDefaults = map[string]string{ + "chain-id": "", + "output": "text", + "node": "tcp://localhost:26657", + "broadcast-mode": "sync", } // ConfigCmd returns a CLI command to interactively create a @@ -56,13 +53,13 @@ func runConfigCmd(cmd *cobra.Command, args []string) error { return fmt.Errorf("wrong number of arguments") } - // Load configuration + // load configuration tree, err := loadConfigFile(cfgFile) if err != nil { return err } - // Print the config and exit + // print the config and exit if len(args) == 0 { s, err := tree.ToTomlString() if err != nil { @@ -73,45 +70,54 @@ func runConfigCmd(cmd *cobra.Command, args []string) error { } key := args[0] - // Get value action + + // get config value for a given key if getAction { switch key { case "trace", "trust-node", "indent": fmt.Println(tree.GetDefault(key, false).(bool)) + default: if defaultValue, ok := configDefaults[key]; ok { fmt.Println(tree.GetDefault(key, defaultValue).(string)) return nil } + return errUnknownConfigKey(key) } + return nil } - // Set value action if len(args) != 2 { return fmt.Errorf("wrong number of arguments") } + value := args[1] + + // set config value for a given key switch key { - case "chain-id", "output", "node": + case "chain-id", "output", "node", "broadcast-mode": tree.Set(key, value) + case "trace", "trust-node", "indent": boolVal, err := strconv.ParseBool(value) if err != nil { return err } + tree.Set(key, boolVal) + default: return errUnknownConfigKey(key) } - // Save configuration to disk + // save configuration to disk if err := saveConfigFile(cfgFile, tree); err != nil { return err } - fmt.Fprintf(os.Stderr, "configuration saved to %s\n", cfgFile) + fmt.Fprintf(os.Stderr, "configuration saved to %s\n", cfgFile) return nil } diff --git a/client/context/broadcast.go b/client/context/broadcast.go index 0503c8e59e..4a754a5292 100644 --- a/client/context/broadcast.go +++ b/client/context/broadcast.go @@ -3,6 +3,7 @@ package context import ( "fmt" + "github.com/cosmos/cosmos-sdk/client" sdk "github.com/cosmos/cosmos-sdk/types" ) @@ -11,29 +12,36 @@ import ( // an intermediate structure which is logged if the context has a logger // defined. func (ctx CLIContext) BroadcastTx(txBytes []byte) (res sdk.TxResponse, err error) { - if ctx.Async { - if res, err = ctx.BroadcastTxAsync(txBytes); err != nil { - return - } - return + switch ctx.BroadcastMode { + case client.BroadcastSync: + res, err = ctx.BroadcastTxSync(txBytes) + + case client.BroadcastAsync: + res, err = ctx.BroadcastTxAsync(txBytes) + + case client.BroadcastBlock: + res, err = ctx.BroadcastTxCommit(txBytes) + + default: + return sdk.TxResponse{}, fmt.Errorf("unsupported return type %s; supported types: sync, async, block", ctx.BroadcastMode) } - if res, err = ctx.BroadcastTxAndAwaitCommit(txBytes); err != nil { - return - } - - return + return res, err } -// BroadcastTxAndAwaitCommit broadcasts transaction bytes to a Tendermint node -// and waits for a commit. -func (ctx CLIContext) BroadcastTxAndAwaitCommit(tx []byte) (sdk.TxResponse, error) { +// BroadcastTxCommit broadcasts transaction bytes to a Tendermint node and +// waits for a commit. +// +// NOTE: This should ideally not be used as the request may timeout but the tx +// may still be included in a block. Use BroadcastTxAsync or BroadcastTxSync +// instead. +func (ctx CLIContext) BroadcastTxCommit(txBytes []byte) (sdk.TxResponse, error) { node, err := ctx.GetNode() if err != nil { return sdk.TxResponse{}, err } - res, err := node.BroadcastTxCommit(tx) + res, err := node.BroadcastTxCommit(txBytes) if err != nil { return sdk.NewResponseFormatBroadcastTxCommit(res), err } @@ -46,35 +54,29 @@ func (ctx CLIContext) BroadcastTxAndAwaitCommit(tx []byte) (sdk.TxResponse, erro return sdk.NewResponseFormatBroadcastTxCommit(res), fmt.Errorf(res.DeliverTx.Log) } - return sdk.NewResponseFormatBroadcastTxCommit(res), err + return sdk.NewResponseFormatBroadcastTxCommit(res), nil } -// BroadcastTxSync broadcasts transaction bytes to a Tendermint node synchronously. -func (ctx CLIContext) BroadcastTxSync(tx []byte) (sdk.TxResponse, error) { +// BroadcastTxSync broadcasts transaction bytes to a Tendermint node +// synchronously (i.e. returns after CheckTx execution). +func (ctx CLIContext) BroadcastTxSync(txBytes []byte) (sdk.TxResponse, error) { node, err := ctx.GetNode() if err != nil { return sdk.TxResponse{}, err } - res, err := node.BroadcastTxSync(tx) - if err != nil { - return sdk.NewResponseFormatBroadcastTx(res), err - } - + res, err := node.BroadcastTxSync(txBytes) return sdk.NewResponseFormatBroadcastTx(res), err } -// BroadcastTxAsync broadcasts transaction bytes to a Tendermint node asynchronously. -func (ctx CLIContext) BroadcastTxAsync(tx []byte) (sdk.TxResponse, error) { +// BroadcastTxAsync broadcasts transaction bytes to a Tendermint node +// asynchronously (i.e. returns immediately). +func (ctx CLIContext) BroadcastTxAsync(txBytes []byte) (sdk.TxResponse, error) { node, err := ctx.GetNode() if err != nil { return sdk.TxResponse{}, err } - res, err := node.BroadcastTxAsync(tx) - if err != nil { - return sdk.NewResponseFormatBroadcastTx(res), err - } - + res, err := node.BroadcastTxAsync(txBytes) return sdk.NewResponseFormatBroadcastTx(res), err } diff --git a/client/context/context.go b/client/context/context.go index f85f9ba4cd..e9d7e6782c 100644 --- a/client/context/context.go +++ b/client/context/context.go @@ -44,7 +44,7 @@ type CLIContext struct { AccountStore string TrustNode bool UseLedger bool - Async bool + BroadcastMode string PrintResponse bool Verifier tmlite.Verifier VerifierHome string @@ -90,7 +90,7 @@ func NewCLIContext() CLIContext { Height: viper.GetInt64(client.FlagHeight), TrustNode: viper.GetBool(client.FlagTrustNode), UseLedger: viper.GetBool(client.FlagUseLedger), - Async: viper.GetBool(client.FlagAsync), + BroadcastMode: viper.GetString(client.FlagBroadcastMode), PrintResponse: viper.GetBool(client.FlagPrintResponse), Verifier: verifier, Simulate: viper.GetBool(client.FlagDryRun), @@ -248,6 +248,13 @@ func (ctx CLIContext) WithFromAddress(addr sdk.AccAddress) CLIContext { return ctx } +// WithBroadcastMode returns a copy of the context with an updated broadcast +// mode. +func (ctx CLIContext) WithBroadcastMode(mode string) CLIContext { + ctx.BroadcastMode = mode + return ctx +} + // PrintOutput prints output while respecting output and indent flags // NOTE: pass in marshalled structs that have been unmarshaled // because this function will panic on marshaling errors diff --git a/client/flags.go b/client/flags.go index fb52481fdf..28e5cc7b9c 100644 --- a/client/flags.go +++ b/client/flags.go @@ -18,11 +18,20 @@ const ( DefaultGasLimit = 200000 GasFlagAuto = "auto" + // BroadcastBlock defines a tx broadcasting mode where the client waits for + // the tx to be committed in a block. + BroadcastBlock = "block" + // BroadcastSync defines a tx broadcasting mode where the client waits for + // a CheckTx execution response only. + BroadcastSync = "sync" + // BroadcastAsync defines a tx broadcasting mode where the client returns + // immediately. + BroadcastAsync = "async" + FlagUseLedger = "ledger" FlagChainID = "chain-id" FlagNode = "node" FlagHeight = "height" - FlagGas = "gas" FlagGasAdjustment = "gas-adjustment" FlagTrustNode = "trust-node" FlagFrom = "from" @@ -32,7 +41,7 @@ const ( FlagMemo = "memo" FlagFees = "fees" FlagGasPrices = "gas-prices" - FlagAsync = "async" + FlagBroadcastMode = "broadcast-mode" FlagPrintResponse = "print-response" FlagDryRun = "dry-run" FlagGenerateOnly = "generate-only" @@ -80,7 +89,7 @@ func PostCommands(cmds ...*cobra.Command) []*cobra.Command { c.Flags().String(FlagNode, "tcp://localhost:26657", ": to tendermint rpc interface for this chain") c.Flags().Bool(FlagUseLedger, false, "Use a connected Ledger device") c.Flags().Float64(FlagGasAdjustment, DefaultGasAdjustment, "adjustment factor to be multiplied against the estimate returned by the tx simulation; if the gas limit is set manually this flag is ignored ") - c.Flags().Bool(FlagAsync, false, "broadcast transactions asynchronously") + c.Flags().StringP(FlagBroadcastMode, "b", BroadcastSync, "Transaction broadcasting mode (sync|async|block)") c.Flags().Bool(FlagPrintResponse, true, "return tx response (only works with async = false)") c.Flags().Bool(FlagTrustNode, true, "Trust connected full node (don't verify proofs for responses)") c.Flags().Bool(FlagDryRun, false, "ignore the --gas flag and perform a simulation of a transaction, but don't broadcast it") diff --git a/client/lcd/swagger-ui/swagger.yaml b/client/lcd/swagger-ui/swagger.yaml index 999a2ee039..720d5d043a 100644 --- a/client/lcd/swagger-ui/swagger.yaml +++ b/client/lcd/swagger-ui/swagger.yaml @@ -250,14 +250,14 @@ paths: parameters: - in: body name: txBroadcast - description: The tx must be a signed StdTx. The supported return types includes `"block"`(return after tx commit), `"sync"`(return afer CheckTx) and `"async"`(return right away). + description: The tx must be a signed StdTx. The supported broadcast modes include `"block"`(return after tx commit), `"sync"`(return afer CheckTx) and `"async"`(return right away). required: true schema: type: object properties: tx: $ref: "#/definitions/StdTx" - return: + mode: type: string example: block responses: diff --git a/client/lcd/test_helpers.go b/client/lcd/test_helpers.go index ef871f5bf8..0828df5528 100644 --- a/client/lcd/test_helpers.go +++ b/client/lcd/test_helpers.go @@ -651,7 +651,7 @@ func getAccount(t *testing.T, port string, addr sdk.AccAddress) auth.Account { // POST /tx/broadcast Send a signed Tx func doBroadcast(t *testing.T, port string, tx auth.StdTx) (*http.Response, string) { - txReq := clienttx.BroadcastReq{Tx: tx, Return: "block"} + txReq := clienttx.BroadcastReq{Tx: tx, Mode: "block"} req, err := cdc.MarshalJSON(txReq) require.Nil(t, err) diff --git a/client/tx/broadcast.go b/client/tx/broadcast.go index d80475c638..15afff00fa 100644 --- a/client/tx/broadcast.go +++ b/client/tx/broadcast.go @@ -18,19 +18,10 @@ import ( "github.com/cosmos/cosmos-sdk/codec" ) -const ( - // Returns with the response from CheckTx. - flagSync = "sync" - // Returns right away, with no response - flagAsync = "async" - // Only returns error if mempool.BroadcastTx errs (ie. problem with the app) or if we timeout waiting for tx to commit. - flagBlock = "block" -) - // BroadcastReq defines a tx broadcasting request. type BroadcastReq struct { - Tx auth.StdTx `json:"tx"` - Return string `json:"return"` + Tx auth.StdTx `json:"tx"` + Mode string `json:"mode"` } // BroadcastTxRequest implements a tx broadcasting handler that is responsible @@ -58,23 +49,9 @@ func BroadcastTxRequest(cliCtx context.CLIContext, cdc *codec.Codec) http.Handle return } - var res interface{} - switch req.Return { - case flagBlock: - res, err = cliCtx.BroadcastTx(txBytes) - - case flagSync: - res, err = cliCtx.BroadcastTxSync(txBytes) - - case flagAsync: - res, err = cliCtx.BroadcastTxAsync(txBytes) - - default: - rest.WriteErrorResponse(w, http.StatusInternalServerError, - "unsupported return type. supported types: block, sync, async") - return - } + cliCtx = cliCtx.WithBroadcastMode(req.Mode) + res, err := cliCtx.BroadcastTx(txBytes) if err != nil { rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) return diff --git a/client/utils/utils.go b/client/utils/utils.go index 8bd8671fb5..20867df071 100644 --- a/client/utils/utils.go +++ b/client/utils/utils.go @@ -80,6 +80,7 @@ func CompleteAndBroadcastTxCLI(txBldr authtxb.TxBuilder, cliCtx context.CLIConte } else { json = cliCtx.Codec.MustMarshalJSON(stdSignMsg) } + fmt.Fprintf(os.Stderr, "%s\n\n", json) buf := client.BufferStdin() @@ -103,8 +104,11 @@ func CompleteAndBroadcastTxCLI(txBldr authtxb.TxBuilder, cliCtx context.CLIConte // broadcast to a Tendermint node res, err := cliCtx.BroadcastTx(txBytes) - cliCtx.PrintOutput(res) // nolint:errcheck - return err + if err != nil { + return err + } + + return cliCtx.PrintOutput(res) } // EnrichWithGas calculates the gas estimate that would be consumed by the diff --git a/cmd/gaia/cli_test/cli_test.go b/cmd/gaia/cli_test/cli_test.go index 57ac7d703c..1151b908cf 100644 --- a/cmd/gaia/cli_test/cli_test.go +++ b/cmd/gaia/cli_test/cli_test.go @@ -965,6 +965,7 @@ func TestGaiaCLIConfig(t *testing.T) { node := fmt.Sprintf("%s:%s", f.RPCAddr, f.Port) // Set available configuration options + f.CLIConfig("broadcast-mode", "block") f.CLIConfig("node", node) f.CLIConfig("output", "text") f.CLIConfig("trust-node", "true") @@ -974,7 +975,8 @@ func TestGaiaCLIConfig(t *testing.T) { config, err := ioutil.ReadFile(path.Join(f.GCLIHome, "config", "config.toml")) require.NoError(t, err) - expectedConfig := fmt.Sprintf(`chain-id = "%s" + expectedConfig := fmt.Sprintf(`broadcast-mode = "block" +chain-id = "%s" indent = true node = "%s" output = "text" diff --git a/cmd/gaia/cli_test/test_helpers.go b/cmd/gaia/cli_test/test_helpers.go index 04bfb2666d..aac07abb27 100644 --- a/cmd/gaia/cli_test/test_helpers.go +++ b/cmd/gaia/cli_test/test_helpers.go @@ -129,7 +129,9 @@ func InitFixtures(t *testing.T) (f *Fixtures) { // NOTE: GDInit sets the ChainID f.GDInit(keyFoo) + f.CLIConfig("chain-id", f.ChainID) + f.CLIConfig("broadcast-mode", "block") // start an account with tokens f.AddGenesisAccount(f.KeyAddress(keyFoo), startCoins) diff --git a/docs/gaia/gaiacli.md b/docs/gaia/gaiacli.md index f4c59ef83b..6aff38af4b 100644 --- a/docs/gaia/gaiacli.md +++ b/docs/gaia/gaiacli.md @@ -128,6 +128,19 @@ gaiacli keys show --multisig-threshold K name1 name2 name3 [...] For more information regarding how to generate, sign and broadcast transactions with a multi signature account see [Multisig Transactions](#multisig-transactions). +### Tx Broadcasting + +When broadcasting transactions, `gaiacli` accepts a `--broadcast-mode` flag. This +flag can have a value of `sync` (default), `async`, or `block`, where `sync` makes +the client return a CheckTx response, `async` makes the client return immediately, +and `block` makes the client wait for the tx to be committed (or timing out). + +It is important to note that the `block` mode should **not** be used in most +circumstances. This is because broadcasting can timeout but the tx may still be +included in a block. This can result in many undesirable situations. Therefor, it +is best to use `sync` or `async` and query by tx hash to determine when the tx +is included in a block. + ### Fees & Gas Each transaction may either supply fees or gas prices, but not both. diff --git a/types/result.go b/types/result.go index 8ff873bfcf..df88acdf38 100644 --- a/types/result.go +++ b/types/result.go @@ -68,6 +68,7 @@ type TxResponse struct { TxHash string `json:"txhash"` Code uint32 `json:"code,omitempty"` Data []byte `json:"data,omitempty"` + RawLog string `json:"raw_log,omitempty"` Logs ABCIMessageLogs `json:"logs,omitempty"` Info string `json:"info,omitempty"` GasWanted int64 `json:"gas_wanted,omitempty"` @@ -90,6 +91,7 @@ func NewResponseResultTx(res *ctypes.ResultTx, tx Tx) TxResponse { Height: res.Height, Code: res.TxResult.Code, Data: res.TxResult.Data, + RawLog: res.TxResult.Log, Logs: parsedLogs, Info: res.TxResult.Info, GasWanted: res.TxResult.GasWanted, @@ -99,8 +101,44 @@ func NewResponseResultTx(res *ctypes.ResultTx, tx Tx) TxResponse { } } -// NewResponseFormatBroadcastTxCommit returns a TxResponse given a ResultBroadcastTxCommit from tendermint +// NewResponseFormatBroadcastTxCommit returns a TxResponse given a +// ResultBroadcastTxCommit from tendermint. func NewResponseFormatBroadcastTxCommit(res *ctypes.ResultBroadcastTxCommit) TxResponse { + if !res.CheckTx.IsOK() { + return newTxResponseCheckTx(res) + } + + return newTxResponseDeliverTx(res) +} + +func newTxResponseCheckTx(res *ctypes.ResultBroadcastTxCommit) TxResponse { + if res == nil { + return TxResponse{} + } + + var txHash string + if res.Hash != nil { + txHash = res.Hash.String() + } + + parsedLogs, _ := ParseABCILogs(res.CheckTx.Log) + + return TxResponse{ + Height: res.Height, + TxHash: txHash, + Code: res.CheckTx.Code, + Data: res.CheckTx.Data, + RawLog: res.CheckTx.Log, + Logs: parsedLogs, + Info: res.CheckTx.Info, + GasWanted: res.CheckTx.GasWanted, + GasUsed: res.CheckTx.GasUsed, + Tags: TagsToStringTags(res.CheckTx.Tags), + Codespace: res.CheckTx.Codespace, + } +} + +func newTxResponseDeliverTx(res *ctypes.ResultBroadcastTxCommit) TxResponse { if res == nil { return TxResponse{} } @@ -117,6 +155,7 @@ func NewResponseFormatBroadcastTxCommit(res *ctypes.ResultBroadcastTxCommit) TxR TxHash: txHash, Code: res.DeliverTx.Code, Data: res.DeliverTx.Data, + RawLog: res.DeliverTx.Log, Logs: parsedLogs, Info: res.DeliverTx.Info, GasWanted: res.DeliverTx.GasWanted, @@ -124,7 +163,6 @@ func NewResponseFormatBroadcastTxCommit(res *ctypes.ResultBroadcastTxCommit) TxR Tags: TagsToStringTags(res.DeliverTx.Tags), Codespace: res.DeliverTx.Codespace, } - } // NewResponseFormatBroadcastTx returns a TxResponse given a ResultBroadcastTx from tendermint @@ -138,6 +176,7 @@ func NewResponseFormatBroadcastTx(res *ctypes.ResultBroadcastTx) TxResponse { return TxResponse{ Code: res.Code, Data: res.Data.Bytes(), + RawLog: res.Log, Logs: parsedLogs, TxHash: res.Hash.String(), } @@ -163,6 +202,10 @@ func (r TxResponse) String() string { sb.WriteString(fmt.Sprintf(" Data: %s\n", string(r.Data))) } + if r.RawLog != "" { + sb.WriteString(fmt.Sprintf(" Raw Log: %s\n", r.RawLog)) + } + if r.Logs != nil { sb.WriteString(fmt.Sprintf(" Logs: %s\n", r.Logs)) }