diff --git a/cmd/common/display/message.go b/cmd/common/display/message.go index e80dca41ea..eb0660a284 100644 --- a/cmd/common/display/message.go +++ b/cmd/common/display/message.go @@ -4,8 +4,53 @@ import ( "encoding/hex" "encoding/json" "fmt" + + "github.com/kwilteam/kwil-db/core/types/transactions" ) +// TxHashAndExecResponse is meant to combine the "tx_hash" marshalling of +// RespTxHash with a RespTxQuery in an "exec_result" field. +type TxHashAndExecResponse struct { + Hash RespTxHash // embedding breaks MarshalJSON of composing types + QueryResp *RespTxQuery `json:"exec_result"` +} + +// NewTxHashAndExecResponse makes a TxHashAndExecResponse from a TcTxQueryResponse. +func NewTxHashAndExecResponse(resp *transactions.TcTxQueryResponse) *TxHashAndExecResponse { + return &TxHashAndExecResponse{ + Hash: RespTxHash(resp.Hash), + QueryResp: &RespTxQuery{Msg: resp}, + } +} + +func (h *TxHashAndExecResponse) MarshalJSON() ([]byte, error) { + return json.Marshal(struct { + TxHash string `json:"tx_hash"` + QueryResp *RespTxQuery `json:"exec_result"` + }{ + TxHash: h.Hash.Hex(), + QueryResp: h.QueryResp, + }) +} + +// MarshalText deduplicates the tx hash for a compact readable output, unlike +// the JSON marshalling that is meant to be a composition of both RespTxHash and +// RespTxQuery. +func (h TxHashAndExecResponse) MarshalText() ([]byte, error) { + return []byte(fmt.Sprintf(`TxHash: %s +Status: %s +Height: %d +Log: %s`, h.Hash.Hex(), + heightStatus(h.QueryResp.Msg), + h.QueryResp.Msg.Height, + h.QueryResp.Msg.TxResult.Log, + ), + ), nil +} + +var _ MsgFormatter = (*TxHashAndExecResponse)(nil) +var _ MsgFormatter = (*RespTxQuery)(nil) + type TxHashResponse struct { TxHash string `json:"tx_hash"` } @@ -38,3 +83,46 @@ func (s RespString) MarshalJSON() ([]byte, error) { func (s RespString) MarshalText() ([]byte, error) { return []byte(s), nil } + +// RespTxQuery is used to represent a transaction response in cli +type RespTxQuery struct { + Msg *transactions.TcTxQueryResponse +} + +func (r *RespTxQuery) MarshalJSON() ([]byte, error) { + return json.Marshal(struct { + Hash string `json:"hash"` // HEX + Height int64 `json:"height"` + Tx transactions.Transaction `json:"tx"` + TxResult transactions.TransactionResult `json:"tx_result"` + }{ + Hash: hex.EncodeToString(r.Msg.Hash), + Height: r.Msg.Height, + Tx: r.Msg.Tx, + TxResult: r.Msg.TxResult, + }) +} + +func heightStatus(res *transactions.TcTxQueryResponse) string { + status := "failed" + if res.Height == -1 { + status = "pending" + } else if res.TxResult.Code == transactions.CodeOk.Uint32() { + status = "success" + } + return status +} + +func (r *RespTxQuery) MarshalText() ([]byte, error) { + msg := fmt.Sprintf(`Transaction ID: %s +Status: %s +Height: %d +Log: %s`, + hex.EncodeToString(r.Msg.Hash), + heightStatus(r.Msg), + r.Msg.Height, + r.Msg.TxResult.Log, + ) + + return []byte(msg), nil +} diff --git a/cmd/common/display/message_test.go b/cmd/common/display/message_test.go index 75ab91e985..5d20b963f5 100644 --- a/cmd/common/display/message_test.go +++ b/cmd/common/display/message_test.go @@ -2,11 +2,17 @@ package display import ( "bytes" + "encoding/hex" "errors" + "fmt" "io" + "math/big" "os" "testing" + "github.com/kwilteam/kwil-db/core/crypto/auth" + "github.com/kwilteam/kwil-db/core/types/transactions" + "github.com/stretchr/testify/assert" ) @@ -72,3 +78,115 @@ func ExampleRespTxHash_json_withError() { // "error": "an error" // } } + +func getExampleTxQueryResponse() *transactions.TcTxQueryResponse { + secp256k1EpSigHex := "cb3fed7f6ff36e59054c04a831b215e514052753ee353e6fe31d4b4ef736acd6155127db555d3006ba14fcb4c79bbad56c8e63b81a9896319bb053a9e253475800" + secp256k1EpSigBytes, _ := hex.DecodeString(secp256k1EpSigHex) + secpSig := auth.Signature{ + Signature: secp256k1EpSigBytes, + Type: auth.EthPersonalSignAuth, + } + + rawPayload := transactions.ActionExecution{ + DBID: "xf617af1ca774ebbd6d23e8fe12c56d41d25a22d81e88f67c6c6ee0d4", + Action: "create_user", + Arguments: [][]string{ + {"foo", "32"}, + }, + } + + payloadRLP, err := rawPayload.MarshalBinary() + if err != nil { + panic(err) + } + + return &transactions.TcTxQueryResponse{ + Hash: []byte("1024"), + Height: 10, + Tx: transactions.Transaction{ + Body: &transactions.TransactionBody{ + Payload: payloadRLP, + PayloadType: rawPayload.Type(), + Fee: big.NewInt(100), + Nonce: 10, + ChainID: "asdf", + Description: "This is a test transaction for cli", + }, + Serialization: transactions.SignedMsgConcat, + Signature: &secpSig, + }, + TxResult: transactions.TransactionResult{ + Code: 0, + Log: "This is log", + GasUsed: 10, + GasWanted: 10, + Data: nil, + Events: nil, + }, + } +} + +func Example_respTxQuery_text() { + Print(&RespTxQuery{Msg: getExampleTxQueryResponse()}, nil, "text") + // Output: + // Transaction ID: 31303234 + // Status: success + // Height: 10 + // Log: This is log +} + +func Example_respTxQuery_json() { + Print(&RespTxQuery{Msg: getExampleTxQueryResponse()}, nil, "json") + // Output: + // { + // "result": { + // "hash": "31303234", + // "height": 10, + // "tx": { + // "Signature": { + // "signature_bytes": "yz/tf2/zblkFTASoMbIV5RQFJ1PuNT5v4x1LTvc2rNYVUSfbVV0wBroU/LTHm7rVbI5juBqYljGbsFOp4lNHWAA=", + // "signature_type": "secp256k1_ep" + // }, + // "Body": { + // "Description": "This is a test transaction for cli", + // "Payload": "AAH4ULg5eGY2MTdhZjFjYTc3NGViYmQ2ZDIzZThmZTEyYzU2ZDQxZDI1YTIyZDgxZTg4ZjY3YzZjNmVlMGQ0i2NyZWF0ZV91c2VyyMeDZm9vgjMy", + // "PayloadType": "execute_action", + // "Fee": 100, + // "Nonce": 10, + // "ChainID": "asdf" + // }, + // "Serialization": "concat", + // "Sender": null + // }, + // "tx_result": { + // "code": 0, + // "log": "This is log", + // "gas_used": 10, + // "gas_wanted": 10 + // } + // }, + // "error": "" + // } +} + +func Test_TxHashAndExecResponse(t *testing.T) { + hash := []byte{1, 2, 3, 4, 5} + qr := getExampleTxQueryResponse() + qr.Hash = hash + resp := &TxHashAndExecResponse{ + Hash: hash, + QueryResp: &RespTxQuery{Msg: qr}, + } + expectJson := `{"tx_hash":"0102030405","exec_result":{"hash":"0102030405","height":10,"tx":{"Signature":{"signature_bytes":"yz/tf2/zblkFTASoMbIV5RQFJ1PuNT5v4x1LTvc2rNYVUSfbVV0wBroU/LTHm7rVbI5juBqYljGbsFOp4lNHWAA=","signature_type":"secp256k1_ep"},"Body":{"Description":"This is a test transaction for cli","Payload":"AAH4ULg5eGY2MTdhZjFjYTc3NGViYmQ2ZDIzZThmZTEyYzU2ZDQxZDI1YTIyZDgxZTg4ZjY3YzZjNmVlMGQ0i2NyZWF0ZV91c2VyyMeDZm9vgjMy","PayloadType":"execute_action","Fee":100,"Nonce":10,"ChainID":"asdf"},"Serialization":"concat","Sender":null},` + + `"tx_result":{"code":0,"log":"This is log","gas_used":10,"gas_wanted":10}}}` + expectText := "TxHash: 0102030405\nStatus: success\nHeight: 10\nLog: This is log" + + outText, err := resp.MarshalText() + assert.NoError(t, err, "MarshalText should not return error") + assert.Equal(t, expectText, string(outText), "MarshalText should return expected text") + + outJson, err := resp.MarshalJSON() + fmt.Println(string(outJson)) + assert.NoError(t, err, "MarshalJSON should not return error") + assert.Equal(t, expectJson, string(outJson), "MarshalJSON should return expected json") +} diff --git a/cmd/kwil-admin/cmds/utils/query-tx.go b/cmd/kwil-admin/cmds/utils/query-tx.go index 5e244a3c84..84b654ec9f 100644 --- a/cmd/kwil-admin/cmds/utils/query-tx.go +++ b/cmd/kwil-admin/cmds/utils/query-tx.go @@ -35,7 +35,7 @@ func queryTxCmd() *cobra.Command { return display.PrintErr(cmd, err) } - return display.PrintCmd(cmd, &respTxQuery{Msg: res}) + return display.PrintCmd(cmd, &display.RespTxQuery{Msg: res}) }, } @@ -44,12 +44,12 @@ func queryTxCmd() *cobra.Command { return cmd } -// respTxQuery is used to represent a transaction response in cli -type respTxQuery struct { +// RespTxQuery is used to represent a transaction response in cli +type RespTxQuery struct { Msg *transactions.TcTxQueryResponse } -func (r *respTxQuery) MarshalJSON() ([]byte, error) { +func (r *RespTxQuery) MarshalJSON() ([]byte, error) { return json.Marshal(struct { Hash string `json:"hash"` // HEX Height int64 `json:"height"` @@ -63,7 +63,7 @@ func (r *respTxQuery) MarshalJSON() ([]byte, error) { }) } -func (r *respTxQuery) MarshalText() ([]byte, error) { +func (r *RespTxQuery) MarshalText() ([]byte, error) { status := "failed" if r.Msg.Height == -1 { status = "pending" diff --git a/cmd/kwil-cli/cmds/account/transfer.go b/cmd/kwil-cli/cmds/account/transfer.go index 1d7e26ba44..a3150cdecb 100644 --- a/cmd/kwil-cli/cmds/account/transfer.go +++ b/cmd/kwil-cli/cmds/account/transfer.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "math/big" + "time" "github.com/kwilteam/kwil-db/cmd/common/display" "github.com/kwilteam/kwil-db/cmd/kwil-cli/cmds/common" @@ -38,6 +39,15 @@ func transferCmd() *cobra.Command { if err != nil { return display.PrintErr(cmd, fmt.Errorf("transfer failed: %w", err)) } + // If sycnBcast, and we have a txHash (error or not), do a query-tx. + if len(txHash) != 0 && syncBcast { + time.Sleep(500 * time.Millisecond) // otherwise it says not found at first + resp, err := cl.TxQuery(ctx, txHash) + if err != nil { + return display.PrintErr(cmd, fmt.Errorf("tx query failed: %w", err)) + } + return display.PrintCmd(cmd, display.NewTxHashAndExecResponse(resp)) + } return display.PrintCmd(cmd, display.RespTxHash(txHash)) }) }, diff --git a/cmd/kwil-cli/cmds/database/batch.go b/cmd/kwil-cli/cmds/database/batch.go index a852f5f05c..fac586b448 100644 --- a/cmd/kwil-cli/cmds/database/batch.go +++ b/cmd/kwil-cli/cmds/database/batch.go @@ -5,6 +5,7 @@ import ( "fmt" "os" "strings" + "time" "github.com/kwilteam/kwil-db/cmd/common/display" "github.com/kwilteam/kwil-db/cmd/kwil-cli/cmds/common" @@ -93,13 +94,21 @@ func batchCmd() *cobra.Command { return display.PrintErr(cmd, fmt.Errorf("error creating action inputs: %w", err)) } - resp, err := cl.ExecuteAction(ctx, dbid, strings.ToLower(action), tuples, + txHash, err := cl.ExecuteAction(ctx, dbid, strings.ToLower(action), tuples, client.WithNonce(nonceOverride), client.WithSyncBroadcast(syncBcast)) if err != nil { return display.PrintErr(cmd, fmt.Errorf("error executing action: %w", err)) } - - return display.PrintCmd(cmd, display.RespTxHash(resp)) + // If sycnBcast, and we have a txHash (error or not), do a query-tx. + if len(txHash) != 0 && syncBcast { + time.Sleep(500 * time.Millisecond) // otherwise it says not found at first + resp, err := cl.TxQuery(ctx, txHash) + if err != nil { + return display.PrintErr(cmd, fmt.Errorf("tx query failed: %w", err)) + } + return display.PrintCmd(cmd, display.NewTxHashAndExecResponse(resp)) + } + return display.PrintCmd(cmd, display.RespTxHash(txHash)) }) }, } diff --git a/cmd/kwil-cli/cmds/database/deploy.go b/cmd/kwil-cli/cmds/database/deploy.go index 3b869cd93a..94a34e108e 100644 --- a/cmd/kwil-cli/cmds/database/deploy.go +++ b/cmd/kwil-cli/cmds/database/deploy.go @@ -6,6 +6,7 @@ import ( "fmt" "io" "os" + "time" "github.com/kwilteam/kuneiform/kfparser" "github.com/kwilteam/kwil-db/cmd/common/display" @@ -69,7 +70,15 @@ func deployCmd() *cobra.Command { if err != nil { return display.PrintErr(cmd, fmt.Errorf("failed to deploy database: %w", err)) } - + // If sycnBcast, and we have a txHash (error or not), do a query-tx. + if len(txHash) != 0 && syncBcast { + time.Sleep(500 * time.Millisecond) // otherwise it says not found at first + resp, err := cl.TxQuery(ctx, txHash) + if err != nil { + return display.PrintErr(cmd, fmt.Errorf("tx query failed: %w", err)) + } + return display.PrintCmd(cmd, display.NewTxHashAndExecResponse(resp)) + } return display.PrintCmd(cmd, display.RespTxHash(txHash)) }) }, diff --git a/cmd/kwil-cli/cmds/database/drop.go b/cmd/kwil-cli/cmds/database/drop.go index ff7db66d8c..c68ae858cd 100644 --- a/cmd/kwil-cli/cmds/database/drop.go +++ b/cmd/kwil-cli/cmds/database/drop.go @@ -3,6 +3,7 @@ package database import ( "context" "fmt" + "time" "github.com/kwilteam/kwil-db/cmd/common/display" "github.com/kwilteam/kwil-db/cmd/kwil-cli/cmds/common" @@ -33,13 +34,21 @@ func dropCmd() *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { return common.DialClient(cmd.Context(), cmd, 0, func(ctx context.Context, cl common.Client, conf *config.KwilCliConfig) error { var err error - resp, err := cl.DropDatabase(ctx, args[0], client.WithNonce(nonceOverride), + txHash, err := cl.DropDatabase(ctx, args[0], client.WithNonce(nonceOverride), client.WithSyncBroadcast(syncBcast)) if err != nil { return display.PrintErr(cmd, fmt.Errorf("error dropping database: %w", err)) } - - return display.PrintCmd(cmd, display.RespTxHash(resp)) + // If sycnBcast, and we have a txHash (error or not), do a query-tx. + if len(txHash) != 0 && syncBcast { + time.Sleep(500 * time.Millisecond) // otherwise it says not found at first + resp, err := cl.TxQuery(ctx, txHash) + if err != nil { + return display.PrintErr(cmd, fmt.Errorf("tx query failed: %w", err)) + } + return display.PrintCmd(cmd, display.NewTxHashAndExecResponse(resp)) + } + return display.PrintCmd(cmd, display.RespTxHash(txHash)) }) }, } diff --git a/cmd/kwil-cli/cmds/database/execute.go b/cmd/kwil-cli/cmds/database/execute.go index 2e7904a2cf..3631b935af 100644 --- a/cmd/kwil-cli/cmds/database/execute.go +++ b/cmd/kwil-cli/cmds/database/execute.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "strings" + "time" "github.com/kwilteam/kwil-db/cmd/common/display" "github.com/kwilteam/kwil-db/cmd/kwil-cli/cmds/common" @@ -62,13 +63,21 @@ func executeCmd() *cobra.Command { // Could actually just directly pass nonce to the client method, // but those methods don't need tx details in the inputs. - resp, err := cl.ExecuteAction(ctx, dbId, lowerName, inputs, + txHash, err := cl.ExecuteAction(ctx, dbId, lowerName, inputs, client.WithNonce(nonceOverride), client.WithSyncBroadcast(syncBcast)) if err != nil { return display.PrintErr(cmd, fmt.Errorf("error executing database: %w", err)) } - - return display.PrintCmd(cmd, display.RespTxHash(resp)) + // If sycnBcast, and we have a txHash (error or not), do a query-tx. + if len(txHash) != 0 && syncBcast { + time.Sleep(500 * time.Millisecond) // otherwise it says not found at first + resp, err := cl.TxQuery(ctx, txHash) + if err != nil { + return display.PrintErr(cmd, fmt.Errorf("tx query failed: %w", err)) + } + return display.PrintCmd(cmd, display.NewTxHashAndExecResponse(resp)) + } + return display.PrintCmd(cmd, display.RespTxHash(txHash)) }) }, } diff --git a/cmd/kwil-cli/cmds/utils/message.go b/cmd/kwil-cli/cmds/utils/message.go index c0ccd30d28..b73afb1840 100644 --- a/cmd/kwil-cli/cmds/utils/message.go +++ b/cmd/kwil-cli/cmds/utils/message.go @@ -2,14 +2,12 @@ package utils import ( "bytes" - "encoding/hex" "encoding/json" "fmt" "reflect" "github.com/kwilteam/kwil-db/cmd/kwil-cli/config" "github.com/kwilteam/kwil-db/core/types" - "github.com/kwilteam/kwil-db/core/types/transactions" ) type respChainInfo struct { @@ -33,46 +31,6 @@ Hash: %s return []byte(msg), nil } -// respTxQuery is used to represent a transaction response in cli -type respTxQuery struct { - Msg *transactions.TcTxQueryResponse -} - -func (r *respTxQuery) MarshalJSON() ([]byte, error) { - return json.Marshal(struct { - Hash string `json:"hash"` // HEX - Height int64 `json:"height"` - Tx transactions.Transaction `json:"tx"` - TxResult transactions.TransactionResult `json:"tx_result"` - }{ - Hash: hex.EncodeToString(r.Msg.Hash), - Height: r.Msg.Height, - Tx: r.Msg.Tx, - TxResult: r.Msg.TxResult, - }) -} - -func (r *respTxQuery) MarshalText() ([]byte, error) { - status := "failed" - if r.Msg.Height == -1 { - status = "pending" - } else if r.Msg.TxResult.Code == transactions.CodeOk.Uint32() { - status = "success" - } - - msg := fmt.Sprintf(`Transaction ID: %s -Status: %s -Height: %d -Log: %s`, - hex.EncodeToString(r.Msg.Hash), - status, - r.Msg.Height, - r.Msg.TxResult.Log, - ) - - return []byte(msg), nil -} - // respKwilCliConfig is used to represent a kwil-cli config in cli type respKwilCliConfig struct { cfg *config.KwilCliConfig diff --git a/cmd/kwil-cli/cmds/utils/message_test.go b/cmd/kwil-cli/cmds/utils/message_test.go index 5813adb3fd..44e743aa67 100644 --- a/cmd/kwil-cli/cmds/utils/message_test.go +++ b/cmd/kwil-cli/cmds/utils/message_test.go @@ -1,15 +1,10 @@ package utils import ( - "encoding/hex" - "math/big" - "github.com/kwilteam/kwil-db/cmd/common/display" "github.com/kwilteam/kwil-db/cmd/kwil-cli/config" "github.com/kwilteam/kwil-db/core/crypto" - "github.com/kwilteam/kwil-db/core/crypto/auth" "github.com/kwilteam/kwil-db/core/types" - "github.com/kwilteam/kwil-db/core/types/transactions" ) func Example_respChainInfo_text() { @@ -44,97 +39,6 @@ func Example_respChainInfo_json() { // "error": "" // } } - -func getExampleTxQueryResponse() *transactions.TcTxQueryResponse { - secp256k1EpSigHex := "cb3fed7f6ff36e59054c04a831b215e514052753ee353e6fe31d4b4ef736acd6155127db555d3006ba14fcb4c79bbad56c8e63b81a9896319bb053a9e253475800" - secp256k1EpSigBytes, _ := hex.DecodeString(secp256k1EpSigHex) - secpSig := auth.Signature{ - Signature: secp256k1EpSigBytes, - Type: auth.EthPersonalSignAuth, - } - - rawPayload := transactions.ActionExecution{ - DBID: "xf617af1ca774ebbd6d23e8fe12c56d41d25a22d81e88f67c6c6ee0d4", - Action: "create_user", - Arguments: [][]string{ - {"foo", "32"}, - }, - } - - payloadRLP, err := rawPayload.MarshalBinary() - if err != nil { - panic(err) - } - - return &transactions.TcTxQueryResponse{ - Hash: []byte("1024"), - Height: 10, - Tx: transactions.Transaction{ - Body: &transactions.TransactionBody{ - Payload: payloadRLP, - PayloadType: rawPayload.Type(), - Fee: big.NewInt(100), - Nonce: 10, - ChainID: "asdf", - Description: "This is a test transaction for cli", - }, - Serialization: transactions.SignedMsgConcat, - Signature: &secpSig, - }, - TxResult: transactions.TransactionResult{ - Code: 0, - Log: "This is log", - GasUsed: 10, - GasWanted: 10, - Data: nil, - Events: nil, - }, - } -} - -func Example_respTxQuery_text() { - display.Print(&respTxQuery{Msg: getExampleTxQueryResponse()}, nil, "text") - // Output: - // Transaction ID: 31303234 - // Status: success - // Height: 10 - // Log: This is log -} - -func Example_respTxQuery_json() { - display.Print(&respTxQuery{Msg: getExampleTxQueryResponse()}, nil, "json") - // Output: - // { - // "result": { - // "hash": "31303234", - // "height": 10, - // "tx": { - // "Signature": { - // "signature_bytes": "yz/tf2/zblkFTASoMbIV5RQFJ1PuNT5v4x1LTvc2rNYVUSfbVV0wBroU/LTHm7rVbI5juBqYljGbsFOp4lNHWAA=", - // "signature_type": "secp256k1_ep" - // }, - // "Body": { - // "Description": "This is a test transaction for cli", - // "Payload": "AAH4ULg5eGY2MTdhZjFjYTc3NGViYmQ2ZDIzZThmZTEyYzU2ZDQxZDI1YTIyZDgxZTg4ZjY3YzZjNmVlMGQ0i2NyZWF0ZV91c2VyyMeDZm9vgjMy", - // "PayloadType": "execute_action", - // "Fee": 100, - // "Nonce": 10, - // "ChainID": "asdf" - // }, - // "Serialization": "concat", - // "Sender": null - // }, - // "tx_result": { - // "code": 0, - // "log": "This is log", - // "gas_used": 10, - // "gas_wanted": 10 - // } - // }, - // "error": "" - // } -} - func Example_respKwilCliConfig_text() { pk, err := crypto.GenerateSecp256k1Key() if err != nil { diff --git a/cmd/kwil-cli/cmds/utils/tx_query.go b/cmd/kwil-cli/cmds/utils/tx_query.go index 53c33ad00e..5ac640b619 100644 --- a/cmd/kwil-cli/cmds/utils/tx_query.go +++ b/cmd/kwil-cli/cmds/utils/tx_query.go @@ -28,7 +28,7 @@ func txQueryCmd() *cobra.Command { if err != nil { return display.PrintErr(cmd, fmt.Errorf("error querying transaction: %w", err)) } - return display.PrintCmd(cmd, &respTxQuery{Msg: msg}) + return display.PrintCmd(cmd, &display.RespTxQuery{Msg: msg}) }) }, diff --git a/core/types/transactions/transaction.go b/core/types/transactions/transaction.go index 2da7f3a745..6cc827787a 100644 --- a/core/types/transactions/transaction.go +++ b/core/types/transactions/transaction.go @@ -113,7 +113,7 @@ func CreateTransaction(contents Payload, chainID string, nonce uint64) (*Transac }, nil } -type Transaction struct { +type Transaction struct { // TODO: json tags with lower case, technically a breaking change // Signature is the signature of the transaction. Signature *auth.Signature