From e595382288d5c782bd78e6ccb4e950943d8941d1 Mon Sep 17 00:00:00 2001 From: rhuairahrighairigh Date: Sat, 14 Jul 2018 23:27:56 +0100 Subject: [PATCH] tidy cli commands --- cmd/kvcli/main.go | 15 +- internal/x/paychan/client/cmd/cmd.go | 257 ++++++++++++++++++--------- internal/x/paychan/handler.go | 4 +- internal/x/paychan/types.go | 76 ++++---- 4 files changed, 231 insertions(+), 121 deletions(-) diff --git a/cmd/kvcli/main.go b/cmd/kvcli/main.go index 90aa90e8..4074a523 100644 --- a/cmd/kvcli/main.go +++ b/cmd/kvcli/main.go @@ -17,6 +17,7 @@ import ( bankcmd "github.com/cosmos/cosmos-sdk/x/bank/client/cli" //ibccmd "github.com/cosmos/cosmos-sdk/x/ibc/client/cli" //stakecmd "github.com/cosmos/cosmos-sdk/x/stake/client/cli" + paychancmd "github.com/kava-labs/kava/internal/x/paychan/client/cli" "github.com/kava-labs/kava/internal/app" "github.com/kava-labs/kava/internal/lcd" @@ -57,7 +58,6 @@ func main() { rootCmd.AddCommand( client.PostCommands( // this just wraps the input cmds with common flags bankcmd.SendTxCmd(cdc), - // paychan commands... //ibccmd.IBCTransferCmd(cdc), //ibccmd.IBCRelayCmd(cdc), //stakecmd.GetCmdCreateValidator(cdc), @@ -66,6 +66,19 @@ func main() { //stakecmd.GetCmdUnbond(cdc), )...) + paychanCmd := &cobra.Command{ + Use: "paychan", + Short: "Payment channel subcommands", + } + stakeCmd.AddCommand( + client.PostCommands( + paychancmd.CreatePaychanCmd(cdc), + paychancmd.GenerateNewStateCmd(cdc), + paychancmd.ClosePaychanCmd(cdc), + )...) + rootCmd.AddCommand( + paychanCmd, + ) // add proxy, version and key info rootCmd.AddCommand( client.LineBreak, diff --git a/internal/x/paychan/client/cmd/cmd.go b/internal/x/paychan/client/cmd/cmd.go index 0c704389..398dc2df 100644 --- a/internal/x/paychan/client/cmd/cmd.go +++ b/internal/x/paychan/client/cmd/cmd.go @@ -1,30 +1,37 @@ package cli import ( + "encoding/base64" "fmt" + "github.com/pkg/errors" + "github.com/spf13/cobra" + "github.com/spf13/viper" + "github.com/cosmos/cosmos-sdk/client/context" + "github.com/cosmos/cosmos-sdk/client/keys" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/wire" - "github.com/spf13/cobra" - //"github.com/cosmos/cosmos-sdk/x/auth" + "github.com/cosmos/cosmos-sdk/x/auth" + "github.com/kava-labs/kava/internal/x/paychan" ) // list of functions that return pointers to cobra commands // No local storage needed for cli acting as a sender +// Currently minimum set of cli commands are implemented: +// create paychan - create and fund +// generate new paychan state - print a half signed close tx (sender signs) +// close paychan - close using state (receiver signs) + +// Future cli commands: // create paychan // close paychan // get paychan(s) // send paychan payment // get balance from receiver -// minimum -// create paychan (sender signs) -// create state update (sender signs) (just a half signed close tx, (json encoded?)) -// close paychan (receiver signs) (provide state update as arg) - func CreatePaychanCmd(cdc *wire.Codec) *cobra.Command { flagTo := "to" flagAmount := "amount" @@ -35,20 +42,9 @@ func CreatePaychanCmd(cdc *wire.Codec) *cobra.Command { Long: "Create a new payment channel from a local address to a remote address, funded with some amount of coins. These coins are removed from the sender account and put into the payment channel.", Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { - // get args: from, to, amount - // create a "client context" stuct populated with info from common flags + + // Create a "client context" stuct populated with info from common flags ctx := context.NewCoreContextFromViper() - // ChainID: chainID, - // Height: viper.GetInt64(client.FlagHeight), - // Gas: viper.GetInt64(client.FlagGas), - // TrustNode: viper.GetBool(client.FlagTrustNode), - // FromAddressName: viper.GetString(client.FlagName), - // NodeURI: nodeURI, - // AccountNumber: viper.GetInt64(client.FlagAccountNumber), - // Sequence: viper.GetInt64(client.FlagSequence), - // Client: rpc, - // Decoder: nil, - // AccountStore: "acc", // Get sender adress senderAddress, err := ctx.GetFromAddress() @@ -73,16 +69,17 @@ func CreatePaychanCmd(cdc *wire.Codec) *cobra.Command { // Create the create channel msg to send // TODO write NewMsgCreate func? msg := paychan.MsgCreate{ - sender: senderAddress, - receiver: receiverAddress, - amount: amount, + Sender: senderAddress, + Receiver: receiverAddress, + Amount: amount, } - // Build and sign the transaction, then broadcast to Tendermint + // Build and sign the transaction, then broadcast to the blockchain res, err := ctx.EnsureSignBuildBroadcast(ctx.FromAddressName, msg, cdc) if err != nil { return err } fmt.Printf("Committed at block %d. Hash: %s\n", res.Height, res.Hash.String()) + return nil }, } cmd.Flags().String(flagTo, "", "Recipient address of the payment channel") @@ -90,34 +87,37 @@ func CreatePaychanCmd(cdc *wire.Codec) *cobra.Command { return cmd } -func CreateNewStateCmd(cdc *wire.Codec) *cobra.Command { +func GenerateNewStateCmd(cdc *wire.Codec) *cobra.Command { flagId := "id" flagTo := "to" flagAmount := "amount" cmd := &cobra.Command{ - Use: "localstate", - Short: "Create a payment channel claim", - Long: "", + Use: "new-state", + Short: "Generate a new payment channel state.", + Long: "Generate a new state for an existing payment channel and print it out. The new state is represented as a half signed close transaction.", Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { - // sender(name) receiver id - // create a "client context" stuct populated with info from common flags + // Create a "client context" stuct populated with info from common flags ctx := context.NewCoreContextFromViper() + // Get sender adress senderAddress, err := ctx.GetFromAddress() if err != nil { return err } - // Get id - id := viper.GetInt(flagId) + + // Get the paychan id + id := viper.GetInt64(flagId) + // Get receiver address toStr := viper.GetString(flagTo) receiverAddress, err := sdk.GetAccAddressBech32(toStr) if err != nil { return err } + // Get channel receiver amount amountString := viper.GetString(flagAmount) amount, err := sdk.ParseCoins(amountString) @@ -125,26 +125,26 @@ func CreateNewStateCmd(cdc *wire.Codec) *cobra.Command { return err } - // create MsgClose - + // create close paychan msg msg := paychan.MsgClose{ - sender: senderAddress, - receiver: receiverAddress, - id: id, - receiverAmount: amount, + Sender: senderAddress, + Receiver: receiverAddress, + Id: id, + ReceiverAmount: amount, } - // half sign it - + // Sign the msg as the sender txBytes, err := EnsureSignBuild(ctx, ctx.FromAddressName, msg, cdc) if err != nil { return err } - // print it out - - fmt.Println(txBytes) - + // Print out the signed msg + fmt.Println("txBytes:", txBytes) + encodedTxBytes := make([]byte, base64.StdEncoding.EncodedLen(len(txBytes))) + base64.StdEncoding.Encode(encodedTxBytes, txBytes) + fmt.Println("base64TxBytes:", encodedTxBytes) + return nil }, } cmd.Flags().Int(flagId, 0, "ID of the payment channel.") @@ -153,50 +153,147 @@ func CreateNewStateCmd(cdc *wire.Codec) *cobra.Command { return cmd } -// sign and build the transaction from the msg -func EnsureSignBuild(ctx context.CoreContext, name string, msg sdk.Msg, cdc *wire.Codec) ([]byte, error) { - - ctx, err = EnsureAccountNumber(ctx) - if err != nil { - return nil, err - } - // default to next sequence number if none provided - ctx, err = EnsureSequence(ctx) - if err != nil { - return nil, err - } - - passphrase, err := ctx.GetPassphraseFromStdin(name) - if err != nil { - return nil, err - } - - txBytes, err := ctx.SignAndBuild(name, passphrase, msg, cdc) - if err != nil { - return nil, err - } - - return txBytes -} - func ClosePaychanCmd(cdc *wire.Codec) *cobra.Command { - flagId := "id" - flagTo := "to" flagState := "state" cmd := &cobra.Command{ Use: "close", - Short: "Close a payment channel", - Long: "", + Short: "Close a payment channel, given a state", + Long: "Close an existing payment channel with a state received from a sender.", Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { - // get sender, reciver, id - // get state - // sign the state tx with receiver - // broadcast to tendermint + ctx := context.NewCoreContextFromViper() + + // Get the sender-signed close tx + state := viper.GetString(flagState) + txBytes, err := base64.StdEncoding.DecodeString(state) + if err != nil { + return err + } + stdTx := auth.StdTx{} + cdc.UnmarshalBinary(txBytes, stdTx) + + // Sign close tx + + // ensure contxt has up to date account and sequence numbers + ctx, err = Ensure(ctx) + if err != nil { + return err + } + // Sign message (asks user for password) + _, sig, err := UserSignMsg(ctx, ctx.FromAddressName, stdTx.Msg) + if err != nil { + return err + } + + // Append signature to close tx + + stdTx.Signatures = append(stdTx.Signatures, sig) + // encode close tx + txBytes, err = cdc.MarshalBinary(stdTx) + if err != nil { + return err + } + + // Broadcast close tx to the blockchain + + res, err := ctx.BroadcastTx(txBytes) + if err != nil { + return err + } + fmt.Printf("Committed at block %d. Hash: %s\n", res.Height, res.Hash.String()) + return nil }, } - //cmd.Flags().String(flagTo, "", "Recipient address of the payment channel") - //cmd.Flags().String(flagAmount, "", "Amount of coins to fund the paymetn channel with") + cmd.Flags().String(flagState, "", "State received from sender.") return cmd } + +// HELPER FUNCTIONS +// This is a partial refactor of cosmos-sdk/client/context. +// Existing API was awkard to use for paychans. + +func Ensure(ctx context.CoreContext) (context.CoreContext, error) { + + ctx, err := context.EnsureAccountNumber(ctx) + if err != nil { + return ctx, err + } + // default to next sequence number if none provided + ctx, err = context.EnsureSequence(ctx) + if err != nil { + return ctx, err + } + return ctx, nil +} + +func UserSignMsg(ctx context.CoreContext, name string, msg sdk.Msg) (signMsg auth.StdSignMsg, stdSig auth.StdSignature, err error) { + + // TODO check how to handle non error return values on error. Returning empty versions doesn't seem right. + + passphrase, err := ctx.GetPassphraseFromStdin(name) + if err != nil { + return signMsg, stdSig, err + } + + // build the Sign Messsage from the Standard Message + chainID := ctx.ChainID + if chainID == "" { + return signMsg, stdSig, errors.Errorf("Chain ID required but not specified") + } + accnum := ctx.AccountNumber + sequence := ctx.Sequence + + signMsg = auth.StdSignMsg{ + ChainID: chainID, + AccountNumbers: []int64{accnum}, + Sequences: []int64{sequence}, + Msg: msg, + Fee: auth.NewStdFee(ctx.Gas, sdk.Coin{}), // TODO run simulate to estimate gas? + } + + keybase, err := keys.GetKeyBase() + if err != nil { + return signMsg, stdSig, err + } + + // sign and build + bz := signMsg.Bytes() + + sig, pubkey, err := keybase.Sign(name, passphrase, bz) + if err != nil { + return signMsg, stdSig, err + } + stdSig = auth.StdSignature{ + PubKey: pubkey, + Signature: sig, + AccountNumber: accnum, + Sequence: sequence, + } + + return signMsg, stdSig, nil +} + +func Build(cdc *wire.Codec, signMsg auth.StdSignMsg, sig auth.StdSignature) ([]byte, error) { + tx := auth.NewStdTx(signMsg.Msg, signMsg.Fee, []auth.StdSignature{sig}) + return cdc.MarshalBinary(tx) +} + +func EnsureSignBuild(ctx context.CoreContext, name string, msg sdk.Msg, cdc *wire.Codec) ([]byte, error) { + //Ensure context has up to date account and sequence numbers + ctx, err := Ensure(ctx) + if err != nil { + return nil, err + } + // Sign message (asks user for password) + signMsg, sig, err := UserSignMsg(ctx, name, msg) + if err != nil { + return nil, err + } + // Create tx and marshal + txBytes, err := Build(cdc, signMsg, sig) + if err != nil { + return nil, err + } + return txBytes, nil +} diff --git a/internal/x/paychan/handler.go b/internal/x/paychan/handler.go index 751fe36a..cf649cbb 100644 --- a/internal/x/paychan/handler.go +++ b/internal/x/paychan/handler.go @@ -25,7 +25,7 @@ func NewHandler(k Keeper) sdk.Handler { // Leaves validation to the keeper methods. func handleMsgCreate(ctx sdk.Context, k Keeper, msg MsgCreate) sdk.Result { // TODO maybe remove tags for first version - tags, err := k.CreatePaychan(ctx, msg.sender, msg.receiver, msg.amount) + tags, err := k.CreatePaychan(ctx, msg.Sender, msg.Receiver, msg.Amount) if err != nil { return err.Result() } @@ -39,7 +39,7 @@ func handleMsgCreate(ctx sdk.Context, k Keeper, msg MsgCreate) sdk.Result { // Leaves validation to the keeper methods. func handleMsgClose(ctx sdk.Context, k Keeper, msg MsgClose) sdk.Result { // TODO maybe remove tags for first version - tags, err := k.ClosePaychan(ctx, msg.sender, msg.receiver, msg.id, msg.receiverAmount) + tags, err := k.ClosePaychan(ctx, msg.Sender, msg.Receiver, msg.Id, msg.ReceiverAmount) if err != nil { return err.Result() } diff --git a/internal/x/paychan/types.go b/internal/x/paychan/types.go index dc7e5129..bb1c42b3 100644 --- a/internal/x/paychan/types.go +++ b/internal/x/paychan/types.go @@ -41,9 +41,9 @@ type Paychan struct { // A message to create a payment channel. type MsgCreate struct { // maybe just wrap a paychan struct - sender sdk.Address - receiver sdk.Address - amount sdk.Coins + Sender sdk.Address + Receiver sdk.Address + Amount sdk.Coins } // Create a new message. @@ -66,9 +66,9 @@ func (msg MsgCreate) GetSignBytes() []byte { ReceiverAddr string `json:"receiver_addr"` Amount sdk.Coins `json:"amount"` }{ - SenderAddr: sdk.MustBech32ifyAcc(msg.sender), - ReceiverAddr: sdk.MustBech32ifyAcc(msg.receiver), - Amount: msg.amount, + SenderAddr: sdk.MustBech32ifyAcc(msg.Sender), + ReceiverAddr: sdk.MustBech32ifyAcc(msg.Receiver), + Amount: msg.Amount, }) if err != nil { panic(err) @@ -81,21 +81,21 @@ func (msg MsgCreate) ValidateBasic() sdk.Error { // Validate without external information (such as account balance) // check if all fields present / not 0 valued - if len(msg.sender) == 0 { - return sdk.ErrInvalidAddress(msg.sender.String()) + if len(msg.Sender) == 0 { + return sdk.ErrInvalidAddress(msg.Sender.String()) } - if len(msg.receiver) == 0 { - return sdk.ErrInvalidAddress(msg.receiver.String()) + if len(msg.Receiver) == 0 { + return sdk.ErrInvalidAddress(msg.Receiver.String()) } - if len(msg.amount) == 0 { - return sdk.ErrInvalidCoins(msg.amount.String()) + if len(msg.Amount) == 0 { + return sdk.ErrInvalidCoins(msg.Amount.String()) } // Check if coins are sorted, non zero, non negative - if !msg.amount.IsValid() { - return sdk.ErrInvalidCoins(msg.amount.String()) + if !msg.Amount.IsValid() { + return sdk.ErrInvalidCoins(msg.Amount.String()) } - if !msg.amount.IsPositive() { - return sdk.ErrInvalidCoins(msg.amount.String()) + if !msg.Amount.IsPositive() { + return sdk.ErrInvalidCoins(msg.Amount.String()) } // TODO check if Address valid? return nil @@ -103,17 +103,17 @@ func (msg MsgCreate) ValidateBasic() sdk.Error { func (msg MsgCreate) GetSigners() []sdk.Address { // Only sender must sign to create a paychan - return []sdk.Address{msg.sender} + return []sdk.Address{msg.Sender} } // A message to close a payment channel. type MsgClose struct { // have to include sender and receiver in msg explicitly (rather than just universal paychanID) // this gives ability to verify signatures with no external information - sender sdk.Address - receiver sdk.Address - id int64 // TODO is another int type better? - receiverAmount sdk.Coins // amount the receiver should get - sender amount implicit with paychan balance + Sender sdk.Address + Receiver sdk.Address + Id int64 // TODO is another int type better? + ReceiverAmount sdk.Coins // amount the receiver should get - sender amount implicit with paychan balance } // func (msg MsgClose) NewMsgClose(sender sdk.Address, receiver sdk.Address, id integer, receiverAmount sdk.Coins) MsgClose { @@ -135,10 +135,10 @@ func (msg MsgClose) GetSignBytes() []byte { Id int64 `json:"id"` ReceiverAmount sdk.Coins `json:"receiver_amount"` }{ - SenderAddr: sdk.MustBech32ifyAcc(msg.sender), - ReceiverAddr: sdk.MustBech32ifyAcc(msg.receiver), - Id: msg.id, - ReceiverAmount: msg.receiverAmount, + SenderAddr: sdk.MustBech32ifyAcc(msg.Sender), + ReceiverAddr: sdk.MustBech32ifyAcc(msg.Receiver), + Id: msg.Id, + ReceiverAmount: msg.ReceiverAmount, }) if err != nil { panic(err) @@ -148,25 +148,25 @@ func (msg MsgClose) GetSignBytes() []byte { func (msg MsgClose) ValidateBasic() sdk.Error { // check if all fields present / not 0 valued - if len(msg.sender) == 0 { - return sdk.ErrInvalidAddress(msg.sender.String()) + if len(msg.Sender) == 0 { + return sdk.ErrInvalidAddress(msg.Sender.String()) } - if len(msg.receiver) == 0 { - return sdk.ErrInvalidAddress(msg.receiver.String()) + if len(msg.Receiver) == 0 { + return sdk.ErrInvalidAddress(msg.Receiver.String()) } - if len(msg.receiverAmount) == 0 { - return sdk.ErrInvalidCoins(msg.receiverAmount.String()) + if len(msg.ReceiverAmount) == 0 { + return sdk.ErrInvalidCoins(msg.ReceiverAmount.String()) } // check id ≥ 0 - if msg.id < 0 { - return sdk.ErrInvalidAddress(strconv.Itoa(int(msg.id))) // TODO implement custom errors + if msg.Id < 0 { + return sdk.ErrInvalidAddress(strconv.Itoa(int(msg.Id))) // TODO implement custom errors } // Check if coins are sorted, non zero, non negative - if !msg.receiverAmount.IsValid() { - return sdk.ErrInvalidCoins(msg.receiverAmount.String()) + if !msg.ReceiverAmount.IsValid() { + return sdk.ErrInvalidCoins(msg.ReceiverAmount.String()) } - if !msg.receiverAmount.IsPositive() { - return sdk.ErrInvalidCoins(msg.receiverAmount.String()) + if !msg.ReceiverAmount.IsPositive() { + return sdk.ErrInvalidCoins(msg.ReceiverAmount.String()) } // TODO check if Address valid? return nil @@ -174,5 +174,5 @@ func (msg MsgClose) ValidateBasic() sdk.Error { func (msg MsgClose) GetSigners() []sdk.Address { // Both sender and receiver must sign in order to close a channel - return []sdk.Address{msg.sender, msg.receiver} + return []sdk.Address{msg.Sender, msg.Receiver} }