package cli import ( "fmt" "io/ioutil" "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" authcmd "github.com/cosmos/cosmos-sdk/x/auth/client/cli" "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 func CreateChannelCmd(cdc *wire.Codec) *cobra.Command { flagTo := "to" flagCoins := "amount" cmd := &cobra.Command{ Use: "create", Short: "Create a new payment channel", Long: "Create a new unidirectional 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 channel.", Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { // Create a "client context" stuct populated with info from common flags ctx := context.NewCoreContextFromViper().WithDecoder(authcmd.GetAccountDecoder(cdc)) // TODO is this needed for channelID // ctx.PrintResponse = true // Get sender adress sender, err := ctx.GetFromAddress() if err != nil { return err } // Get receiver address toStr := viper.GetString(flagTo) receiver, err := sdk.AccAddressFromBech32(toStr) if err != nil { return err } // Get channel funding amount coinsString := viper.GetString(flagCoins) coins, err := sdk.ParseCoins(coinsString) if err != nil { return err } // Create the create channel msg to send msg := paychan.MsgCreate{ Participants: [2]sdk.AccAddress{sender, receiver}, Coins: coins, } err = msg.ValidateBasic() if err != nil { return err } // Build and sign the transaction, then broadcast to the blockchain err = ctx.EnsureSignBuildBroadcast(ctx.FromAddressName, []sdk.Msg{msg}, cdc) if err != nil { return err } return nil }, } cmd.Flags().String(flagTo, "", "Recipient address of the payment channel.") cmd.Flags().String(flagCoins, "", "Amount of coins to fund the payment channel with.") return cmd } func GeneratePaymentCmd(cdc *wire.Codec) *cobra.Command { flagId := "chan-id" flagReceiverAmount := "rec-amt" // amount the receiver should received on closing the channel flagSenderAmount := "sen-amt" flagPaymentFile := "filename" cmd := &cobra.Command{ Use: "pay", Short: "Generate a new payment.", // TODO descriptions Long: "Generate a payment file (json) to send to the receiver as a payment.", Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { // Create a "client context" stuct populated with info from common flags ctx := context.NewCoreContextFromViper().WithDecoder(authcmd.GetAccountDecoder(cdc)) // Get the paychan id id := paychan.ChannelID(viper.GetInt64(flagId)) // TODO make this default to pulling id from chain // Get channel receiver amount senderCoins, err := sdk.ParseCoins(viper.GetString(flagSenderAmount)) if err != nil { return err } // Get channel receiver amount receiverCoins, err := sdk.ParseCoins(viper.GetString(flagReceiverAmount)) if err != nil { return err } // create close paychan msg update := paychan.Update{ ChannelID: id, Payout: paychan.Payout{senderCoins, receiverCoins}, // empty sigs } // Sign the update as the sender keybase, err := keys.GetKeyBase() if err != nil { return err } name := ctx.FromAddressName passphrase, err := ctx.GetPassphraseFromStdin(name) if err != nil { return err } bz := update.GetSignBytes() sig, pubKey, err := keybase.Sign(name, passphrase, bz) if err != nil { return err } update.Sigs = [1]paychan.UpdateSignature{{ PubKey: pubKey, CryptoSignature: sig, }} // Write out the update jsonUpdate, err := wire.MarshalJSONIndent(cdc, update) if err != nil { return err } paymentFile := viper.GetString(flagPaymentFile) err = ioutil.WriteFile(paymentFile, jsonUpdate, 0644) if err != nil { return err } fmt.Printf("Written payment out to %v.\n", paymentFile) return nil }, } cmd.Flags().Int(flagId, 0, "ID of the payment channel.") cmd.Flags().String(flagSenderAmount, "", "Total coins to payout to sender on channel close.") cmd.Flags().String(flagReceiverAmount, "", "Total coins to payout to sender on channel close.") cmd.Flags().String(flagPaymentFile, "payment.json", "File name to write the payment into.") return cmd } func VerifyPaymentCmd(cdc *wire.Codec, paychanStoreName string) *cobra.Command { flagPaymentFile := "payment" cmd := &cobra.Command{ Use: "verify", Short: "Verify a payment file.", Long: "Verify that a received payment can be used to close a channel.", Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { // Create a "client context" stuct populated with info from common flags ctx := context.NewCoreContextFromViper() // read in update bz, err := ioutil.ReadFile(viper.GetString(flagPaymentFile)) if err != nil { // TODO add nice message about how to feed in stdin return err } // decode json var update paychan.Update cdc.UnmarshalJSON(bz, &update) // get the channel from the node res, err := ctx.QueryStore(paychan.GetChannelKey(update.ChannelID), paychanStoreName) if len(res) == 0 || err != nil { return errors.Errorf("channel with ID '%d' does not exist", update.ChannelID) } var channel paychan.Channel cdc.MustUnmarshalBinary(res, &channel) //verify verificationError := paychan.VerifyUpdate(channel, update) // print result if verificationError == nil { fmt.Printf("Payment is valid for channel '%d'.\n", update.ChannelID) } else { fmt.Printf("Payment is NOT valid for channel '%d'.\n", update.ChannelID) fmt.Println(verificationError) } return nil }, } cmd.Flags().String(flagPaymentFile, "payment.json", "File name to read the payment from.") return cmd } func SubmitPaymentCmd(cdc *wire.Codec) *cobra.Command { flagPaymentFile := "payment" cmd := &cobra.Command{ Use: "submit", Short: "Submit a payment to the blockchain to close the channel.", Long: fmt.Sprintf("Submit a payment to the blockchain to either close a channel immediately (if you are the receiver) or after a dispute period of %d blocks (if you are the sender).", paychan.ChannelDisputeTime), Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { // Create a "client context" stuct populated with info from common flags ctx := context.NewCoreContextFromViper().WithDecoder(authcmd.GetAccountDecoder(cdc)) // ctx.PrintResponse = true TODO is this needed for channelID // Get sender adress submitter, err := ctx.GetFromAddress() if err != nil { return err } // read in update bz, err := ioutil.ReadFile(viper.GetString(flagPaymentFile)) if err != nil { return err } // decode json var update paychan.Update cdc.UnmarshalJSON(bz, &update) // Create the create channel msg to send msg := paychan.MsgSubmitUpdate{ Update: update, Submitter: submitter, } err = msg.ValidateBasic() if err != nil { return err } // Build and sign the transaction, then broadcast to the blockchain err = ctx.EnsureSignBuildBroadcast(ctx.FromAddressName, []sdk.Msg{msg}, cdc) if err != nil { return err } return nil }, } cmd.Flags().String(flagPaymentFile, "payment.json", "File to read the payment from.") return cmd } func GetChannelCmd(cdc *wire.Codec, paychanStoreName string) *cobra.Command { flagId := "chan-id" cmd := &cobra.Command{ Use: "get", Short: "Get info on a channel.", Long: "Get information on a non closed channel.", Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { // Create a "client context" stuct populated with info from common flags ctx := context.NewCoreContextFromViper() // Get channel ID id := paychan.ChannelID(viper.GetInt64(flagId)) // Get the channel from the node res, err := ctx.QueryStore(paychan.GetChannelKey(id), paychanStoreName) if len(res) == 0 || err != nil { return errors.Errorf("channel with ID '%d' does not exist", id) } var channel paychan.Channel cdc.MustUnmarshalBinary(res, &channel) // Convert the channel to a json object for pretty printing jsonChannel, err := wire.MarshalJSONIndent(cdc, channel) if err != nil { return err } // print out json channel fmt.Println(string(jsonChannel)) return nil }, } cmd.Flags().Int(flagId, 0, "ID of the payment channel.") return cmd }