mirror of
https://github.com/0glabs/0g-chain.git
synced 2024-12-25 15:55:18 +00:00
Merge pull request #1 from Kava-Labs/add-payment-channels
Add payment channels
This commit is contained in:
commit
8c9406cd35
@ -17,6 +17,7 @@ import (
|
||||
//ibccmd "github.com/cosmos/cosmos-sdk/x/ibc/client/cli"
|
||||
slashingcmd "github.com/cosmos/cosmos-sdk/x/slashing/client/cli"
|
||||
stakecmd "github.com/cosmos/cosmos-sdk/x/stake/client/cli"
|
||||
paychancmd "github.com/kava-labs/kava/internal/x/paychan/client/cmd"
|
||||
|
||||
"github.com/kava-labs/kava/internal/app"
|
||||
//"github.com/kava-labs/kava/internal/lcd"
|
||||
@ -38,7 +39,7 @@ func main() {
|
||||
// add standard rpc commands
|
||||
rpc.AddCommands(rootCmd)
|
||||
|
||||
//Add state commands
|
||||
// Add state commands
|
||||
tendermintCmd := &cobra.Command{
|
||||
Use: "tendermint",
|
||||
Short: "Tendermint state querying subcommands",
|
||||
@ -49,7 +50,7 @@ func main() {
|
||||
)
|
||||
tx.AddCommands(tendermintCmd, cdc)
|
||||
|
||||
//Add IBC commands
|
||||
// Add IBC commands
|
||||
// ibcCmd := &cobra.Command{
|
||||
// Use: "ibc",
|
||||
// Short: "Inter-Blockchain Communication subcommands",
|
||||
@ -75,7 +76,7 @@ func main() {
|
||||
client.LineBreak,
|
||||
)
|
||||
|
||||
//Add stake commands
|
||||
// Add stake commands
|
||||
stakeCmd := &cobra.Command{
|
||||
Use: "stake",
|
||||
Short: "Stake and validation subcommands",
|
||||
@ -101,7 +102,7 @@ func main() {
|
||||
stakeCmd,
|
||||
)
|
||||
|
||||
//Add stake commands
|
||||
// Add gov commands
|
||||
// govCmd := &cobra.Command{
|
||||
// Use: "gov",
|
||||
// Short: "Governance and voting subcommands",
|
||||
@ -122,16 +123,33 @@ func main() {
|
||||
// govCmd,
|
||||
// )
|
||||
|
||||
//Add auth and bank commands
|
||||
// Add auth and bank commands
|
||||
rootCmd.AddCommand(
|
||||
client.GetCommands(
|
||||
authcmd.GetAccountCmd("acc", cdc, authcmd.GetAccountDecoder(cdc)),
|
||||
)...)
|
||||
rootCmd.AddCommand(
|
||||
client.PostCommands(
|
||||
client.PostCommands( // this just wraps the input cmds with common flags
|
||||
bankcmd.SendTxCmd(cdc),
|
||||
)...)
|
||||
|
||||
// Add paychan commands
|
||||
paychanCmd := &cobra.Command{
|
||||
Use: "paychan",
|
||||
Short: "Payment channel subcommand",
|
||||
}
|
||||
paychanCmd.AddCommand(
|
||||
client.PostCommands(
|
||||
paychancmd.CreateChannelCmd(cdc),
|
||||
paychancmd.GetChannelCmd(cdc, "paychan"), // pass in storeKey
|
||||
paychancmd.GeneratePaymentCmd(cdc),
|
||||
paychancmd.VerifyPaymentCmd(cdc, "paychan"), // pass in storeKey
|
||||
paychancmd.SubmitPaymentCmd(cdc),
|
||||
)...)
|
||||
rootCmd.AddCommand(
|
||||
paychanCmd,
|
||||
)
|
||||
|
||||
// add proxy, version and key info
|
||||
rootCmd.AddCommand(
|
||||
keys.Commands(),
|
||||
|
@ -20,6 +20,7 @@ import (
|
||||
//"github.com/cosmos/cosmos-sdk/x/ibc"
|
||||
"github.com/cosmos/cosmos-sdk/x/slashing"
|
||||
"github.com/cosmos/cosmos-sdk/x/stake"
|
||||
"github.com/kava-labs/kava/internal/x/paychan"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -39,16 +40,19 @@ type KavaApp struct {
|
||||
// keys to access the substores
|
||||
keyMain *sdk.KVStoreKey
|
||||
keyAccount *sdk.KVStoreKey
|
||||
|
||||
//keyIBC *sdk.KVStoreKey
|
||||
keyStake *sdk.KVStoreKey
|
||||
keySlashing *sdk.KVStoreKey
|
||||
//keyGov *sdk.KVStoreKey
|
||||
keyFeeCollection *sdk.KVStoreKey
|
||||
keyPaychan *sdk.KVStoreKey
|
||||
|
||||
// keepers
|
||||
accountMapper auth.AccountMapper
|
||||
feeCollectionKeeper auth.FeeCollectionKeeper
|
||||
coinKeeper bank.Keeper
|
||||
paychanKeeper paychan.Keeper
|
||||
//ibcMapper ibc.Mapper
|
||||
stakeKeeper stake.Keeper
|
||||
slashingKeeper slashing.Keeper
|
||||
@ -71,6 +75,7 @@ func NewKavaApp(logger log.Logger, db dbm.DB, traceStore io.Writer, baseAppOptio
|
||||
cdc: cdc,
|
||||
keyMain: sdk.NewKVStoreKey("main"),
|
||||
keyAccount: sdk.NewKVStoreKey("acc"),
|
||||
keyPaychan: sdk.NewKVStoreKey("paychan"),
|
||||
//keyIBC: sdk.NewKVStoreKey("ibc"),
|
||||
keyStake: sdk.NewKVStoreKey("stake"),
|
||||
keySlashing: sdk.NewKVStoreKey("slashing"),
|
||||
@ -87,6 +92,7 @@ func NewKavaApp(logger log.Logger, db dbm.DB, traceStore io.Writer, baseAppOptio
|
||||
|
||||
// Create the keepers
|
||||
app.coinKeeper = bank.NewKeeper(app.accountMapper)
|
||||
app.paychanKeeper = paychan.NewKeeper(app.cdc, app.keyPaychan, app.coinKeeper)
|
||||
//app.ibcMapper = ibc.NewMapper(app.cdc, app.keyIBC, app.RegisterCodespace(ibc.DefaultCodespace))
|
||||
app.stakeKeeper = stake.NewKeeper(app.cdc, app.keyStake, app.coinKeeper, app.RegisterCodespace(stake.DefaultCodespace))
|
||||
app.slashingKeeper = slashing.NewKeeper(app.cdc, app.keySlashing, app.stakeKeeper, app.RegisterCodespace(slashing.DefaultCodespace))
|
||||
@ -98,7 +104,8 @@ func NewKavaApp(logger log.Logger, db dbm.DB, traceStore io.Writer, baseAppOptio
|
||||
AddRoute("bank", bank.NewHandler(app.coinKeeper)).
|
||||
//AddRoute("ibc", ibc.NewHandler(app.ibcMapper, app.coinKeeper)).
|
||||
AddRoute("stake", stake.NewHandler(app.stakeKeeper)).
|
||||
AddRoute("slashing", slashing.NewHandler(app.slashingKeeper))
|
||||
AddRoute("slashing", slashing.NewHandler(app.slashingKeeper)).
|
||||
AddRoute("paychan", paychan.NewHandler(app.paychanKeeper))
|
||||
//AddRoute("gov", gov.NewHandler(app.govKeeper))
|
||||
|
||||
// Set func to initialze the chain from appState in genesis file
|
||||
@ -110,7 +117,7 @@ func NewKavaApp(logger log.Logger, db dbm.DB, traceStore io.Writer, baseAppOptio
|
||||
app.SetAnteHandler(auth.NewAnteHandler(app.accountMapper, app.feeCollectionKeeper))
|
||||
|
||||
// Mount stores
|
||||
app.MountStoresIAVL(app.keyMain, app.keyAccount, app.keyStake, app.keySlashing, app.keyFeeCollection)
|
||||
app.MountStoresIAVL(app.keyMain, app.keyAccount, app.keyStake, app.keySlashing, app.keyFeeCollection, app.keyPaychan)
|
||||
err := app.LoadLatestVersion(app.keyMain)
|
||||
if err != nil {
|
||||
cmn.Exit(err.Error())
|
||||
@ -121,6 +128,7 @@ func NewKavaApp(logger log.Logger, db dbm.DB, traceStore io.Writer, baseAppOptio
|
||||
// Creates a codec for use across the whole app.
|
||||
func CreateKavaAppCodec() *wire.Codec {
|
||||
cdc := wire.NewCodec()
|
||||
paychan.RegisterWire(cdc)
|
||||
//ibc.RegisterWire(cdc)
|
||||
bank.RegisterWire(cdc)
|
||||
stake.RegisterWire(cdc)
|
||||
@ -143,6 +151,7 @@ func (app *KavaApp) BeginBlocker(ctx sdk.Context, req abci.RequestBeginBlock) ab
|
||||
|
||||
// The function baseapp runs on receipt of a EndBlock ABCI message
|
||||
func (app *KavaApp) EndBlocker(ctx sdk.Context, req abci.RequestEndBlock) abci.ResponseEndBlock {
|
||||
paychan.EndBlocker(ctx, app.paychanKeeper)
|
||||
validatorUpdates := stake.EndBlocker(ctx, app.stakeKeeper)
|
||||
|
||||
//tags, _ := gov.EndBlocker(ctx, app.govKeeper)
|
||||
|
@ -1,5 +1,6 @@
|
||||
package app
|
||||
|
||||
/*
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
@ -75,3 +76,4 @@ func TestGenesis(t *testing.T) {
|
||||
res1 = bapp.accountMapper.GetAccount(ctx, baseAcc.Address)
|
||||
assert.Equal(t, acc, res1)
|
||||
}
|
||||
*/
|
||||
|
57
internal/x/paychan/README.md
Normal file
57
internal/x/paychan/README.md
Normal file
@ -0,0 +1,57 @@
|
||||
# Unidrectional Payment Channels
|
||||
|
||||
This module implements simple but feature complete unidirectional payment channels. Channels can be opened by a sender and closed immediately by the receiver, or by the sender subject to a dispute period. There are no top-ups or partial withdrawals (yet). Channels support multiple currencies.
|
||||
|
||||
>Note: This module is still a bit rough around the edges. More feature planned. More test cases needed.
|
||||
|
||||
# Usage
|
||||
|
||||
## Create a channel
|
||||
|
||||
kvcli paychan create --from <your account name> --to <receivers address> --amount 100KVA --chain-id <your chain ID>
|
||||
|
||||
## Send an off-chain payment
|
||||
Send a payment for 10 KVA.
|
||||
|
||||
kvcli paychan pay --from <your account name> --sen-amt 90KVA --rec-amt 10KVA --chan-id <ID of channel> --filename payment.json --chain-id <your chain ID>
|
||||
|
||||
Send the file `payment.json` to your receiver. Then they run the following to verify.
|
||||
|
||||
kvcli paychan verify --filename payment.json
|
||||
|
||||
## Close a channel
|
||||
The receiver can close immediately at any time.
|
||||
|
||||
kvcli paychan submit --from <receiver's account name> --payment payment.json --chain-id <your chain ID>
|
||||
|
||||
The sender can submit a close request, causing the channel will close automatically after a dispute period. During this period a receiver can still close immediately.
|
||||
|
||||
kvcli paychan submit --from <receiver's account name> --payment payment.json --chain-id <your chain ID>
|
||||
|
||||
>Note: The dispute period on the testnet is 30 seconds for ease of testing.
|
||||
|
||||
## Get info on a channel
|
||||
|
||||
kvcli get --chan-id <ID of channel>
|
||||
|
||||
This will print out a channel, if it exists, and any submitted close requests.
|
||||
|
||||
# TODOs
|
||||
|
||||
- in code TODOs
|
||||
- Tidy up - method descriptions, heading comments, remove uneccessary comments, README/docs
|
||||
- Find a better name for Queue - clarify distinction between int slice and abstract queue concept
|
||||
- write some sort of integration test
|
||||
- possible bug in submitting same update repeatedly
|
||||
- find nicer name for payout
|
||||
- add Gas usage
|
||||
- add tags (return channel id on creation)
|
||||
- refactor cmds to be able to test them, then test them
|
||||
- verify doesn’t throw json parsing error on invalid json
|
||||
- can’t submit an update from an unitialised account
|
||||
- pay without a --from returns confusing error
|
||||
- use custom errors instead of using sdk.ErrInternal
|
||||
- split off signatures from update as with txs/msgs - testing easier, code easier to use, doesn't store sigs unecessarily on chain
|
||||
- consider removing pubKey from UpdateSignature - instead let channel module access accountMapper
|
||||
- refactor queue into one object
|
||||
- remove printout during tests caused by mock app initialisation
|
316
internal/x/paychan/client/cmd/cmd.go
Normal file
316
internal/x/paychan/client/cmd/cmd.go
Normal file
@ -0,0 +1,316 @@
|
||||
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 the details of a non closed channel plus any submitted update waiting to be executed.",
|
||||
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))
|
||||
|
||||
// Get any submitted updates from the node
|
||||
res, err = ctx.QueryStore(paychan.GetSubmittedUpdateKey(id), paychanStoreName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Print out the submited update if it exsits
|
||||
if len(res) != 0 {
|
||||
var submittedUpdate paychan.SubmittedUpdate
|
||||
cdc.MustUnmarshalBinary(res, &submittedUpdate)
|
||||
|
||||
// Convert the submitted update to a json object for pretty printing
|
||||
jsonSU, err := wire.MarshalJSONIndent(cdc, submittedUpdate)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// print out json submitted update
|
||||
fmt.Println(string(jsonSU))
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
cmd.Flags().Int(flagId, 0, "ID of the payment channel.")
|
||||
return cmd
|
||||
}
|
7
internal/x/paychan/client/rest/channel-watcher.go
Normal file
7
internal/x/paychan/client/rest/channel-watcher.go
Normal file
@ -0,0 +1,7 @@
|
||||
package lcd
|
||||
|
||||
import ()
|
||||
|
||||
// implement a thing to poll blockchain and handles paychan disputes
|
||||
// needs plugged into LCD - add a "background processes" slot in the LCD run function?
|
||||
// eventually LCD could evolve into paychan (network) daemon
|
24
internal/x/paychan/client/rest/rest.go
Normal file
24
internal/x/paychan/client/rest/rest.go
Normal file
@ -0,0 +1,24 @@
|
||||
package rest
|
||||
|
||||
import (
|
||||
//"github.com/gorilla/mux"
|
||||
//"github.com/tendermint/go-crypto/keys"
|
||||
//"github.com/cosmos/cosmos-sdk/client/context"
|
||||
//"github.com/cosmos/cosmos-sdk/wire"
|
||||
)
|
||||
|
||||
/*
|
||||
// RegisterRoutes registers paychan-related REST handlers to a router
|
||||
func RegisterRoutes(ctx context.CoreContext, r *mux.Router, cdc *wire.Codec, kb keys.Keybase) {
|
||||
//r.HandleFunc("/accounts/{address}/send", SendRequestHandlerFn(cdc, kb, ctx)).Methods("POST")
|
||||
}
|
||||
|
||||
// handler functions ...
|
||||
// create paychan
|
||||
// close paychan
|
||||
// get paychan(s)
|
||||
// send paychan payment
|
||||
// get balance from receiver
|
||||
// get balance from local storage
|
||||
// handle incoming payment
|
||||
*/
|
5
internal/x/paychan/doc.go
Normal file
5
internal/x/paychan/doc.go
Normal file
@ -0,0 +1,5 @@
|
||||
/*
|
||||
High level package documentation.
|
||||
TODO Explain how the payment channels are implemented.
|
||||
*/
|
||||
package paychan
|
35
internal/x/paychan/endblocker.go
Normal file
35
internal/x/paychan/endblocker.go
Normal file
@ -0,0 +1,35 @@
|
||||
package paychan
|
||||
|
||||
import (
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
)
|
||||
|
||||
func EndBlocker(ctx sdk.Context, k Keeper) sdk.Tags {
|
||||
var err sdk.Error
|
||||
var channelTags sdk.Tags
|
||||
tags := sdk.EmptyTags()
|
||||
|
||||
// Iterate through submittedUpdatesQueue
|
||||
// TODO optimise so it doesn't pull every channel update from DB every block
|
||||
q := k.getSubmittedUpdatesQueue(ctx)
|
||||
var sUpdate SubmittedUpdate
|
||||
var found bool
|
||||
|
||||
for _, id := range q {
|
||||
// close the channel if the update has reached its execution time.
|
||||
// Using >= in case some are somehow missed.
|
||||
sUpdate, found = k.getSubmittedUpdate(ctx, id)
|
||||
if !found {
|
||||
panic("can't find element in queue that should exist")
|
||||
}
|
||||
if ctx.BlockHeight() >= sUpdate.ExecutionTime {
|
||||
k.removeFromSubmittedUpdatesQueue(ctx, sUpdate.ChannelID)
|
||||
channelTags, err = k.closeChannel(ctx, sUpdate.Update)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
tags.AppendTags(channelTags)
|
||||
}
|
||||
}
|
||||
return tags
|
||||
}
|
59
internal/x/paychan/endblocker_test.go
Normal file
59
internal/x/paychan/endblocker_test.go
Normal file
@ -0,0 +1,59 @@
|
||||
package paychan
|
||||
|
||||
import (
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestEndBlocker(t *testing.T) {
|
||||
// TODO test that endBlocker doesn't close channels before the execution time
|
||||
|
||||
// SETUP
|
||||
accountSeeds := []string{"senderSeed", "receiverSeed"}
|
||||
ctx, _, channelKeeper, addrs, _, _, _ := createMockApp(accountSeeds)
|
||||
sender := addrs[0]
|
||||
receiver := addrs[1]
|
||||
coins := sdk.Coins{sdk.NewCoin("KVA", 10)}
|
||||
|
||||
// create new channel
|
||||
channelID := ChannelID(0) // should be 0 as first channel
|
||||
channel := Channel{
|
||||
ID: channelID,
|
||||
Participants: [2]sdk.AccAddress{sender, receiver},
|
||||
Coins: coins,
|
||||
}
|
||||
channelKeeper.setChannel(ctx, channel)
|
||||
|
||||
// create closing update and submittedUpdate
|
||||
payout := Payout{sdk.Coins{sdk.NewCoin("KVA", 3)}, sdk.Coins{sdk.NewCoin("KVA", 7)}}
|
||||
update := Update{
|
||||
ChannelID: channelID,
|
||||
Payout: payout,
|
||||
//Sigs: [1]crypto.Signature{},
|
||||
}
|
||||
sUpdate := SubmittedUpdate{
|
||||
Update: update,
|
||||
ExecutionTime: 0, // current blocktime
|
||||
}
|
||||
// Set empty submittedUpdatesQueue TODO work out proper genesis initialisation
|
||||
channelKeeper.setSubmittedUpdatesQueue(ctx, SubmittedUpdatesQueue{})
|
||||
// flag channel for closure
|
||||
channelKeeper.addToSubmittedUpdatesQueue(ctx, sUpdate)
|
||||
|
||||
// ACTION
|
||||
EndBlocker(ctx, channelKeeper)
|
||||
|
||||
// CHECK RESULTS
|
||||
// ideally just check if keeper.channelClose was called, but can't
|
||||
// writing endBlocker to accept an interface of which keeper is implementation would make this possible
|
||||
// check channel is gone
|
||||
_, found := channelKeeper.getChannel(ctx, channelID)
|
||||
assert.False(t, found)
|
||||
// check queue is empty, NOTE: due to encoding, an empty queue (underneath just an int slice) will be decoded as nil slice rather than an empty slice
|
||||
suq := channelKeeper.getSubmittedUpdatesQueue(ctx)
|
||||
assert.Equal(t, SubmittedUpdatesQueue(nil), suq)
|
||||
// check submittedUpdate is gone
|
||||
_, found = channelKeeper.getSubmittedUpdate(ctx, channelID)
|
||||
assert.False(t, found)
|
||||
}
|
62
internal/x/paychan/handler.go
Normal file
62
internal/x/paychan/handler.go
Normal file
@ -0,0 +1,62 @@
|
||||
package paychan
|
||||
|
||||
import (
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
// NewHandler returns a handler for "paychan" type messages.
|
||||
// Called when adding routes to a newly created app.
|
||||
func NewHandler(k Keeper) sdk.Handler {
|
||||
return func(ctx sdk.Context, msg sdk.Msg) sdk.Result {
|
||||
switch msg := msg.(type) {
|
||||
case MsgCreate:
|
||||
return handleMsgCreate(ctx, k, msg)
|
||||
case MsgSubmitUpdate:
|
||||
return handleMsgSubmitUpdate(ctx, k, msg)
|
||||
default:
|
||||
errMsg := "Unrecognized paychan Msg type: " + reflect.TypeOf(msg).Name()
|
||||
return sdk.ErrUnknownRequest(errMsg).Result()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle MsgCreate
|
||||
// Leaves validation to the keeper methods.
|
||||
func handleMsgCreate(ctx sdk.Context, k Keeper, msg MsgCreate) sdk.Result {
|
||||
tags, err := k.CreateChannel(ctx, msg.Participants[0], msg.Participants[len(msg.Participants)-1], msg.Coins)
|
||||
if err != nil {
|
||||
return err.Result()
|
||||
}
|
||||
// TODO any other information that should be returned in Result?
|
||||
return sdk.Result{
|
||||
Tags: tags,
|
||||
}
|
||||
}
|
||||
|
||||
// Handle MsgSubmitUpdate
|
||||
// Leaves validation to the keeper methods.
|
||||
func handleMsgSubmitUpdate(ctx sdk.Context, k Keeper, msg MsgSubmitUpdate) sdk.Result {
|
||||
var err sdk.Error
|
||||
tags := sdk.EmptyTags()
|
||||
|
||||
// TODO refactor signer detection - move to keeper or find nicer setup
|
||||
channel, _ := k.getChannel(ctx, msg.Update.ChannelID)
|
||||
participants := channel.Participants
|
||||
|
||||
// if only sender signed
|
||||
if reflect.DeepEqual(msg.Submitter, participants[0]) {
|
||||
tags, err = k.InitCloseChannelBySender(ctx, msg.Update)
|
||||
// else if receiver signed
|
||||
} else if reflect.DeepEqual(msg.Submitter, participants[len(participants)-1]) {
|
||||
tags, err = k.CloseChannelByReceiver(ctx, msg.Update)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err.Result()
|
||||
}
|
||||
// These tags can be used by clients to subscribe to channel close attempts
|
||||
return sdk.Result{
|
||||
Tags: tags,
|
||||
}
|
||||
}
|
375
internal/x/paychan/keeper.go
Normal file
375
internal/x/paychan/keeper.go
Normal file
@ -0,0 +1,375 @@
|
||||
package paychan
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
"github.com/cosmos/cosmos-sdk/wire"
|
||||
"github.com/cosmos/cosmos-sdk/x/bank"
|
||||
)
|
||||
|
||||
// Keeper of the paychan store
|
||||
// Handles validation internally. Does not rely on calling code to do validation.
|
||||
// Aim to keep public methods safe, private ones not necessaily.
|
||||
// Keepers contain main business logic of the module.
|
||||
type Keeper struct {
|
||||
storeKey sdk.StoreKey
|
||||
cdc *wire.Codec // needed to serialize objects before putting them in the store
|
||||
coinKeeper bank.Keeper
|
||||
|
||||
//codespace sdk.CodespaceType TODO custom errors
|
||||
}
|
||||
|
||||
// Called when creating new app.
|
||||
func NewKeeper(cdc *wire.Codec, key sdk.StoreKey, ck bank.Keeper) Keeper {
|
||||
keeper := Keeper{
|
||||
storeKey: key,
|
||||
cdc: cdc,
|
||||
coinKeeper: ck,
|
||||
//codespace: codespace,
|
||||
}
|
||||
return keeper
|
||||
}
|
||||
|
||||
// Create a new payment channel and lock up sender funds.
|
||||
func (k Keeper) CreateChannel(ctx sdk.Context, sender sdk.AccAddress, receiver sdk.AccAddress, coins sdk.Coins) (sdk.Tags, sdk.Error) {
|
||||
|
||||
// Check addresses valid (Technicaly don't need to check sender address is valid as SubtractCoins checks)
|
||||
if len(sender) == 0 {
|
||||
return nil, sdk.ErrInvalidAddress(sender.String())
|
||||
}
|
||||
if len(receiver) == 0 {
|
||||
return nil, sdk.ErrInvalidAddress(receiver.String())
|
||||
}
|
||||
// check coins are sorted and positive (disallow channels with zero balance)
|
||||
if !coins.IsValid() {
|
||||
return nil, sdk.ErrInvalidCoins(coins.String())
|
||||
}
|
||||
if !coins.IsPositive() {
|
||||
return nil, sdk.ErrInvalidCoins(coins.String())
|
||||
}
|
||||
|
||||
// subtract coins from sender
|
||||
_, tags, err := k.coinKeeper.SubtractCoins(ctx, sender, coins)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Calculate next id
|
||||
id := k.getNewChannelID(ctx)
|
||||
// create new Paychan struct
|
||||
channel := Channel{
|
||||
ID: id,
|
||||
Participants: [2]sdk.AccAddress{sender, receiver},
|
||||
Coins: coins,
|
||||
}
|
||||
// save to db
|
||||
k.setChannel(ctx, channel)
|
||||
|
||||
// TODO add to tags
|
||||
|
||||
return tags, err
|
||||
}
|
||||
|
||||
// Initiate the close of a payment channel, subject to dispute period.
|
||||
func (k Keeper) InitCloseChannelBySender(ctx sdk.Context, update Update) (sdk.Tags, sdk.Error) {
|
||||
// This is roughly the default path for non unidirectional channels
|
||||
|
||||
// get the channel
|
||||
channel, found := k.getChannel(ctx, update.ChannelID)
|
||||
if !found {
|
||||
return nil, sdk.ErrInternal("Channel doesn't exist")
|
||||
}
|
||||
err := VerifyUpdate(channel, update)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
q := k.getSubmittedUpdatesQueue(ctx)
|
||||
if q.Contains(update.ChannelID) {
|
||||
// Someone has previously tried to update channel
|
||||
|
||||
// In bidirectional channels the new update is compared against existing and replaces it if it has a higher sequence number.
|
||||
|
||||
// existingSUpdate, found := k.getSubmittedUpdate(ctx, update.ChannelID)
|
||||
// if !found {
|
||||
// panic("can't find element in queue that should exist")
|
||||
// }
|
||||
// k.addToSubmittedUpdatesQueue(ctx, k.applyNewUpdate(existingSUpdate, update))
|
||||
|
||||
// However in unidirectional case, only the sender can close a channel this way. No clear need for them to be able to submit an update replacing a previous one they sent, so don't allow it.
|
||||
// TODO tags
|
||||
// TODO custom errors
|
||||
sdk.ErrInternal("Sender can't submit an update for channel if one has already been submitted.")
|
||||
} else {
|
||||
// No one has tried to update channel
|
||||
submittedUpdate := SubmittedUpdate{
|
||||
Update: update,
|
||||
ExecutionTime: ctx.BlockHeight() + ChannelDisputeTime, //TODO check what exactly BlockHeight refers to
|
||||
}
|
||||
k.addToSubmittedUpdatesQueue(ctx, submittedUpdate)
|
||||
}
|
||||
|
||||
tags := sdk.EmptyTags() // TODO tags
|
||||
|
||||
return tags, nil
|
||||
}
|
||||
|
||||
// Immediately close a channel.
|
||||
func (k Keeper) CloseChannelByReceiver(ctx sdk.Context, update Update) (sdk.Tags, sdk.Error) {
|
||||
|
||||
// get the channel
|
||||
channel, found := k.getChannel(ctx, update.ChannelID)
|
||||
if !found {
|
||||
return nil, sdk.ErrInternal("Channel doesn't exist")
|
||||
}
|
||||
err := VerifyUpdate(channel, update)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Check if there is an update in the queue already
|
||||
q := k.getSubmittedUpdatesQueue(ctx)
|
||||
if q.Contains(update.ChannelID) {
|
||||
// Someone has previously tried to update channel but receiver has final say
|
||||
k.removeFromSubmittedUpdatesQueue(ctx, update.ChannelID)
|
||||
}
|
||||
|
||||
tags, err := k.closeChannel(ctx, update)
|
||||
|
||||
return tags, err
|
||||
}
|
||||
|
||||
// Main function that compare updates against each other.
|
||||
// Pure function, Not needed in unidirectional case.
|
||||
// func (k Keeper) applyNewUpdate(existingSUpdate SubmittedUpdate, proposedUpdate Update) SubmittedUpdate {
|
||||
// var returnUpdate SubmittedUpdate
|
||||
|
||||
// if existingSUpdate.Sequence > proposedUpdate.Sequence {
|
||||
// // update accepted
|
||||
// returnUpdate = SubmittedUpdate{
|
||||
// Update: proposedUpdate,
|
||||
// ExecutionTime: existingSUpdate.ExecutionTime, // FIXME any new update proposal should be subject to full dispute period from submission
|
||||
// }
|
||||
// } else {
|
||||
// // update rejected
|
||||
// returnUpdate = existingSUpdate
|
||||
// }
|
||||
// return returnUpdate
|
||||
// }
|
||||
|
||||
func VerifyUpdate(channel Channel, update Update) sdk.Error {
|
||||
|
||||
// Check the num of payout participants match channel participants
|
||||
if len(update.Payout) != len(channel.Participants) {
|
||||
return sdk.ErrInternal("Payout doesn't match number of channel participants")
|
||||
}
|
||||
// Check each coins are valid
|
||||
for _, coins := range update.Payout {
|
||||
if !coins.IsValid() {
|
||||
return sdk.ErrInternal("Payout coins aren't formatted correctly")
|
||||
}
|
||||
}
|
||||
// Check payout coins are each not negative (can be zero though)
|
||||
if !update.Payout.IsNotNegative() {
|
||||
return sdk.ErrInternal("Payout cannot be negative")
|
||||
}
|
||||
// Check payout sums to match channel.Coins
|
||||
if !channel.Coins.IsEqual(update.Payout.Sum()) {
|
||||
return sdk.ErrInternal("Payout amount doesn't match channel amount")
|
||||
}
|
||||
// Check sender signature is OK
|
||||
if !verifySignatures(channel, update) {
|
||||
return sdk.ErrInternal("Signature on update not valid")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// unsafe close channel - doesn't check if update matches existing channel
|
||||
// TODO make safer?
|
||||
func (k Keeper) closeChannel(ctx sdk.Context, update Update) (sdk.Tags, sdk.Error) {
|
||||
var err sdk.Error
|
||||
var tags sdk.Tags
|
||||
|
||||
channel, _ := k.getChannel(ctx, update.ChannelID)
|
||||
// TODO check channel exists and participants matches update payout length
|
||||
|
||||
// Add coins to sender and receiver
|
||||
// TODO check for possible errors first to avoid coins being half paid out?
|
||||
for i, coins := range update.Payout {
|
||||
_, tags, err = k.coinKeeper.AddCoins(ctx, channel.Participants[i], coins)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
k.deleteChannel(ctx, update.ChannelID)
|
||||
|
||||
return tags, nil
|
||||
}
|
||||
|
||||
func verifySignatures(channel Channel, update Update) bool {
|
||||
// In non unidirectional channels there will be more than one signature to check
|
||||
|
||||
signBytes := update.GetSignBytes()
|
||||
|
||||
address := channel.Participants[0]
|
||||
pubKey := update.Sigs[0].PubKey
|
||||
cryptoSig := update.Sigs[0].CryptoSignature
|
||||
|
||||
// Check public key submitted with update signature matches the account address
|
||||
valid := bytes.Equal(pubKey.Address(), address) &&
|
||||
// Check the signature is correct
|
||||
pubKey.VerifyBytes(signBytes, cryptoSig)
|
||||
return valid
|
||||
|
||||
}
|
||||
|
||||
// =========================================== SUBMITTED UPDATES QUEUE
|
||||
|
||||
func (k Keeper) addToSubmittedUpdatesQueue(ctx sdk.Context, sUpdate SubmittedUpdate) {
|
||||
// always overwrite prexisting values - leave paychan logic to higher levels
|
||||
// get current queue
|
||||
q := k.getSubmittedUpdatesQueue(ctx)
|
||||
// append ID to queue
|
||||
if !q.Contains(sUpdate.ChannelID) {
|
||||
q = append(q, sUpdate.ChannelID)
|
||||
}
|
||||
// set queue
|
||||
k.setSubmittedUpdatesQueue(ctx, q)
|
||||
// store submittedUpdate
|
||||
k.setSubmittedUpdate(ctx, sUpdate)
|
||||
}
|
||||
func (k Keeper) removeFromSubmittedUpdatesQueue(ctx sdk.Context, channelID ChannelID) {
|
||||
// get current queue
|
||||
q := k.getSubmittedUpdatesQueue(ctx)
|
||||
// remove id
|
||||
q.RemoveMatchingElements(channelID)
|
||||
// set queue
|
||||
k.setSubmittedUpdatesQueue(ctx, q)
|
||||
// delete submittedUpdate
|
||||
k.deleteSubmittedUpdate(ctx, channelID)
|
||||
}
|
||||
|
||||
func (k Keeper) getSubmittedUpdatesQueue(ctx sdk.Context) SubmittedUpdatesQueue {
|
||||
// load from DB
|
||||
store := ctx.KVStore(k.storeKey)
|
||||
bz := store.Get(k.getSubmittedUpdatesQueueKey())
|
||||
|
||||
var suq SubmittedUpdatesQueue // if the submittedUpdatesQueue not found then return an empty one
|
||||
if bz != nil {
|
||||
// unmarshal
|
||||
k.cdc.MustUnmarshalBinary(bz, &suq)
|
||||
}
|
||||
return suq
|
||||
|
||||
}
|
||||
func (k Keeper) setSubmittedUpdatesQueue(ctx sdk.Context, suq SubmittedUpdatesQueue) {
|
||||
store := ctx.KVStore(k.storeKey)
|
||||
// marshal
|
||||
bz := k.cdc.MustMarshalBinary(suq)
|
||||
// write to db
|
||||
key := k.getSubmittedUpdatesQueueKey()
|
||||
store.Set(key, bz)
|
||||
}
|
||||
func (k Keeper) getSubmittedUpdatesQueueKey() []byte {
|
||||
return []byte("submittedUpdatesQueue")
|
||||
}
|
||||
|
||||
// ============= SUBMITTED UPDATES
|
||||
// These are keyed by the IDs of their associated Channels
|
||||
// This section deals with only setting and getting
|
||||
|
||||
func (k Keeper) getSubmittedUpdate(ctx sdk.Context, channelID ChannelID) (SubmittedUpdate, bool) {
|
||||
|
||||
// load from DB
|
||||
store := ctx.KVStore(k.storeKey)
|
||||
bz := store.Get(GetSubmittedUpdateKey(channelID))
|
||||
|
||||
var sUpdate SubmittedUpdate
|
||||
if bz == nil {
|
||||
return sUpdate, false
|
||||
}
|
||||
// unmarshal
|
||||
k.cdc.MustUnmarshalBinary(bz, &sUpdate)
|
||||
// return
|
||||
return sUpdate, true
|
||||
}
|
||||
|
||||
// Store payment channel struct in blockchain store.
|
||||
func (k Keeper) setSubmittedUpdate(ctx sdk.Context, sUpdate SubmittedUpdate) {
|
||||
store := ctx.KVStore(k.storeKey)
|
||||
// marshal
|
||||
bz := k.cdc.MustMarshalBinary(sUpdate) // panics if something goes wrong
|
||||
// write to db
|
||||
key := GetSubmittedUpdateKey(sUpdate.ChannelID)
|
||||
store.Set(key, bz) // panics if something goes wrong
|
||||
}
|
||||
|
||||
func (k Keeper) deleteSubmittedUpdate(ctx sdk.Context, channelID ChannelID) {
|
||||
store := ctx.KVStore(k.storeKey)
|
||||
store.Delete(GetSubmittedUpdateKey(channelID))
|
||||
// TODO does this have return values? What happens when key doesn't exist?
|
||||
}
|
||||
func GetSubmittedUpdateKey(channelID ChannelID) []byte {
|
||||
return []byte(fmt.Sprintf("submittedUpdate:%d", channelID))
|
||||
}
|
||||
|
||||
// ========================================== CHANNELS
|
||||
|
||||
// Reteive a payment channel struct from the blockchain store.
|
||||
func (k Keeper) getChannel(ctx sdk.Context, channelID ChannelID) (Channel, bool) {
|
||||
// load from DB
|
||||
store := ctx.KVStore(k.storeKey)
|
||||
bz := store.Get(GetChannelKey(channelID))
|
||||
|
||||
var channel Channel
|
||||
if bz == nil {
|
||||
return channel, false
|
||||
}
|
||||
// unmarshal
|
||||
k.cdc.MustUnmarshalBinary(bz, &channel)
|
||||
// return
|
||||
return channel, true
|
||||
}
|
||||
|
||||
// Store payment channel struct in blockchain store.
|
||||
func (k Keeper) setChannel(ctx sdk.Context, channel Channel) {
|
||||
store := ctx.KVStore(k.storeKey)
|
||||
// marshal
|
||||
bz := k.cdc.MustMarshalBinary(channel) // panics if something goes wrong
|
||||
// write to db
|
||||
key := GetChannelKey(channel.ID)
|
||||
store.Set(key, bz) // panics if something goes wrong
|
||||
}
|
||||
|
||||
func (k Keeper) deleteChannel(ctx sdk.Context, channelID ChannelID) {
|
||||
store := ctx.KVStore(k.storeKey)
|
||||
store.Delete(GetChannelKey(channelID))
|
||||
// TODO does this have return values? What happens when key doesn't exist?
|
||||
}
|
||||
|
||||
func (k Keeper) getNewChannelID(ctx sdk.Context) ChannelID {
|
||||
// get last channel ID
|
||||
var lastID ChannelID
|
||||
store := ctx.KVStore(k.storeKey)
|
||||
bz := store.Get(getLastChannelIDKey())
|
||||
if bz == nil {
|
||||
lastID = -1 // TODO is just setting to zero if uninitialized ok?
|
||||
} else {
|
||||
k.cdc.MustUnmarshalBinary(bz, &lastID)
|
||||
}
|
||||
// increment to create new one
|
||||
newID := lastID + 1
|
||||
bz = k.cdc.MustMarshalBinary(newID)
|
||||
// set last channel id again
|
||||
store.Set(getLastChannelIDKey(), bz)
|
||||
// return
|
||||
return newID
|
||||
}
|
||||
|
||||
func GetChannelKey(channelID ChannelID) []byte {
|
||||
return []byte(fmt.Sprintf("channel:%d", channelID))
|
||||
}
|
||||
func getLastChannelIDKey() []byte {
|
||||
return []byte("lastChannelID")
|
||||
}
|
315
internal/x/paychan/keeper_test.go
Normal file
315
internal/x/paychan/keeper_test.go
Normal file
@ -0,0 +1,315 @@
|
||||
package paychan
|
||||
|
||||
import (
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestKeeper(t *testing.T) {
|
||||
t.Run("CreateChannel", func(t *testing.T) {
|
||||
|
||||
// TODO test for receiver account not existing (OK) and sender not existing (not ok)
|
||||
|
||||
accountSeeds := []string{"senderSeed", "receiverSeed"}
|
||||
const (
|
||||
senderAccountIndex int = 0
|
||||
receiverAccountIndex int = 1
|
||||
)
|
||||
_, addrs, _, _ := createTestGenAccounts(accountSeeds, sdk.Coins{}) // pure function
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
sender sdk.AccAddress
|
||||
receiver sdk.AccAddress
|
||||
coins sdk.Coins
|
||||
shouldCreateChannel bool
|
||||
shouldError bool
|
||||
}{
|
||||
{
|
||||
"HappyPath",
|
||||
addrs[senderAccountIndex],
|
||||
addrs[receiverAccountIndex],
|
||||
sdk.Coins{sdk.NewCoin("KVA", 10)},
|
||||
true,
|
||||
false,
|
||||
},
|
||||
{
|
||||
"NilAddress",
|
||||
sdk.AccAddress{},
|
||||
sdk.AccAddress{},
|
||||
sdk.Coins{sdk.NewCoin("KVA", 10)},
|
||||
false,
|
||||
true,
|
||||
},
|
||||
{
|
||||
"NilCoins",
|
||||
addrs[senderAccountIndex],
|
||||
addrs[receiverAccountIndex],
|
||||
sdk.Coins{},
|
||||
false,
|
||||
true,
|
||||
},
|
||||
{
|
||||
"NegativeCoins",
|
||||
addrs[senderAccountIndex],
|
||||
addrs[receiverAccountIndex],
|
||||
sdk.Coins{sdk.NewCoin("KVA", -57)},
|
||||
false,
|
||||
true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
////// SETUP
|
||||
// create basic mock app
|
||||
ctx, coinKeeper, channelKeeper, addrs, _, _, genAccFunding := createMockApp(accountSeeds)
|
||||
//
|
||||
////// ACTION
|
||||
_, err := channelKeeper.CreateChannel(ctx, testCase.sender, testCase.receiver, testCase.coins)
|
||||
|
||||
//
|
||||
////// CHECK RESULTS
|
||||
// Check error
|
||||
if testCase.shouldError {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
// Check if channel exists and is correct
|
||||
channelID := ChannelID(0) // should be 0 as first channel
|
||||
createdChan, found := channelKeeper.getChannel(ctx, channelID)
|
||||
|
||||
if testCase.shouldCreateChannel {
|
||||
expectedChan := Channel{
|
||||
ID: channelID,
|
||||
Participants: [2]sdk.AccAddress{testCase.sender, testCase.receiver},
|
||||
Coins: testCase.coins,
|
||||
}
|
||||
|
||||
// channel exists and correct
|
||||
assert.True(t, found)
|
||||
assert.Equal(t, expectedChan, createdChan)
|
||||
// check coins deducted from sender
|
||||
assert.Equal(t, genAccFunding.Minus(testCase.coins), coinKeeper.GetCoins(ctx, testCase.sender))
|
||||
// check no coins deducted from receiver
|
||||
assert.Equal(t, genAccFunding, coinKeeper.GetCoins(ctx, testCase.receiver))
|
||||
// check next global channelID incremented
|
||||
assert.Equal(t, ChannelID(1), channelKeeper.getNewChannelID(ctx))
|
||||
} else {
|
||||
// channel doesn't exist
|
||||
assert.False(t, found)
|
||||
assert.Equal(t, Channel{}, createdChan)
|
||||
// check no coins deducted from sender
|
||||
assert.Equal(t, genAccFunding, coinKeeper.GetCoins(ctx, addrs[senderAccountIndex]))
|
||||
// check no coins deducted from receiver
|
||||
assert.Equal(t, genAccFunding, coinKeeper.GetCoins(ctx, addrs[receiverAccountIndex]))
|
||||
// check next global channelID not incremented
|
||||
assert.Equal(t, ChannelID(0), channelKeeper.getNewChannelID(ctx))
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("CloseChannelByReceiver", func(t *testing.T) {
|
||||
// TODO convert to table driven and add more test cases
|
||||
// channel exists or not (assume channels correct)
|
||||
// various Updates
|
||||
// submittedUpdates existing or not (assume they are valid)
|
||||
|
||||
// SETUP
|
||||
accountSeeds := []string{"senderSeed", "receiverSeed"}
|
||||
const (
|
||||
senderAccountIndex int = 0
|
||||
receiverAccountIndex int = 1
|
||||
)
|
||||
ctx, coinKeeper, channelKeeper, addrs, pubKeys, privKeys, genAccFunding := createMockApp(accountSeeds)
|
||||
|
||||
coins := sdk.Coins{sdk.NewCoin("KVA", 10)}
|
||||
|
||||
// create new channel
|
||||
channelID := ChannelID(0) // should be 0 as first channel
|
||||
channel := Channel{
|
||||
ID: channelID,
|
||||
Participants: [2]sdk.AccAddress{addrs[senderAccountIndex], addrs[receiverAccountIndex]},
|
||||
Coins: coins,
|
||||
}
|
||||
channelKeeper.setChannel(ctx, channel)
|
||||
|
||||
// create closing update
|
||||
payout := Payout{sdk.Coins{sdk.NewCoin("KVA", 3)}, sdk.Coins{sdk.NewCoin("KVA", 7)}}
|
||||
update := Update{
|
||||
ChannelID: channelID,
|
||||
Payout: payout,
|
||||
// empty sig
|
||||
}
|
||||
cryptoSig, _ := privKeys[senderAccountIndex].Sign(update.GetSignBytes())
|
||||
update.Sigs = [1]UpdateSignature{UpdateSignature{
|
||||
PubKey: pubKeys[senderAccountIndex],
|
||||
CryptoSignature: cryptoSig,
|
||||
}}
|
||||
|
||||
// Set empty submittedUpdatesQueue TODO work out proper genesis initialisation
|
||||
channelKeeper.setSubmittedUpdatesQueue(ctx, SubmittedUpdatesQueue{})
|
||||
|
||||
// ACTION
|
||||
_, err := channelKeeper.CloseChannelByReceiver(ctx, update)
|
||||
|
||||
// CHECK RESULTS
|
||||
// no error
|
||||
assert.NoError(t, err)
|
||||
// coins paid out
|
||||
senderPayout := payout[senderAccountIndex]
|
||||
assert.Equal(t, genAccFunding.Plus(senderPayout), coinKeeper.GetCoins(ctx, addrs[senderAccountIndex]))
|
||||
receiverPayout := payout[receiverAccountIndex]
|
||||
assert.Equal(t, genAccFunding.Plus(receiverPayout), coinKeeper.GetCoins(ctx, addrs[receiverAccountIndex]))
|
||||
// channel deleted
|
||||
_, found := channelKeeper.getChannel(ctx, channelID)
|
||||
assert.False(t, found)
|
||||
|
||||
})
|
||||
|
||||
t.Run("InitCloseChannelBySender", func(t *testing.T) {
|
||||
|
||||
// TODO do some documentation here
|
||||
// Ideally this should mock calls to ctx.store.Get/Set - test the side effects without being dependent on implementatino details
|
||||
// TODO test correct behaviour when a submittedUpdate already exists
|
||||
|
||||
accountSeeds := []string{"senderSeed", "receiverSeed", "notInChannelSeed"}
|
||||
const (
|
||||
senderAccountIndex int = 0
|
||||
receiverAccountIndex int = 1
|
||||
otherAccountIndex int = 2
|
||||
)
|
||||
chanID := ChannelID(0)
|
||||
|
||||
type testUpdate struct { // A parameterised version of an Update for use in specifying test cases.
|
||||
channelID ChannelID // channelID of submitted update
|
||||
payout Payout // payout of submitted update
|
||||
pubKeyAccountIndex int // pubkey of signature of submitted update
|
||||
sigAccountIndex int // crypto signature of signature of submitted update
|
||||
}
|
||||
testCases := []struct {
|
||||
name string
|
||||
setupChannel bool
|
||||
updateToSubmit testUpdate
|
||||
expectedSubmittedUpdate string // "empty" or "sameAsSubmitted"
|
||||
shouldError bool
|
||||
}{
|
||||
{
|
||||
"HappyPath",
|
||||
true,
|
||||
testUpdate{chanID, Payout{sdk.Coins{sdk.NewCoin("KVA", 3)}, sdk.Coins{sdk.NewCoin("KVA", 7)}}, senderAccountIndex, senderAccountIndex},
|
||||
"sameAsSubmited",
|
||||
false,
|
||||
},
|
||||
{
|
||||
"NoChannel",
|
||||
false,
|
||||
testUpdate{chanID, Payout{sdk.Coins{sdk.NewCoin("KVA", 3)}, sdk.Coins{sdk.NewCoin("KVA", 7)}}, senderAccountIndex, senderAccountIndex},
|
||||
"empty",
|
||||
true,
|
||||
},
|
||||
{
|
||||
"NoCoins",
|
||||
true,
|
||||
testUpdate{chanID, Payout{sdk.Coins{}}, senderAccountIndex, senderAccountIndex},
|
||||
"empty",
|
||||
true,
|
||||
},
|
||||
{
|
||||
"NegativeCoins",
|
||||
true,
|
||||
testUpdate{chanID, Payout{sdk.Coins{sdk.NewCoin("KVA", -5)}, sdk.Coins{sdk.NewCoin("KVA", 15)}}, senderAccountIndex, senderAccountIndex},
|
||||
"empty",
|
||||
true,
|
||||
},
|
||||
{
|
||||
"TooManyCoins",
|
||||
true,
|
||||
testUpdate{chanID, Payout{sdk.Coins{sdk.NewCoin("KVA", 100)}, sdk.Coins{sdk.NewCoin("KVA", 7)}}, senderAccountIndex, senderAccountIndex},
|
||||
"empty",
|
||||
true,
|
||||
},
|
||||
{
|
||||
"WrongSignature",
|
||||
true,
|
||||
testUpdate{chanID, Payout{sdk.Coins{sdk.NewCoin("KVA", 3)}, sdk.Coins{sdk.NewCoin("KVA", 7)}}, senderAccountIndex, otherAccountIndex},
|
||||
"empty",
|
||||
true,
|
||||
},
|
||||
{
|
||||
"WrongPubKey",
|
||||
true,
|
||||
testUpdate{chanID, Payout{sdk.Coins{sdk.NewCoin("KVA", 3)}, sdk.Coins{sdk.NewCoin("KVA", 7)}}, otherAccountIndex, senderAccountIndex},
|
||||
"empty",
|
||||
true,
|
||||
},
|
||||
{
|
||||
"ReceiverSigned",
|
||||
true,
|
||||
testUpdate{chanID, Payout{sdk.Coins{sdk.NewCoin("KVA", 3)}, sdk.Coins{sdk.NewCoin("KVA", 7)}}, receiverAccountIndex, receiverAccountIndex},
|
||||
"empty",
|
||||
true,
|
||||
},
|
||||
}
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
|
||||
// SETUP
|
||||
ctx, _, channelKeeper, addrs, pubKeys, privKeys, _ := createMockApp(accountSeeds)
|
||||
// Set empty submittedUpdatesQueue TODO work out proper genesis initialisation
|
||||
channelKeeper.setSubmittedUpdatesQueue(ctx, SubmittedUpdatesQueue{})
|
||||
// create new channel
|
||||
if testCase.setupChannel {
|
||||
channel := Channel{
|
||||
ID: chanID, // should be 0 as first channel
|
||||
Participants: [2]sdk.AccAddress{addrs[senderAccountIndex], addrs[receiverAccountIndex]},
|
||||
Coins: sdk.Coins{sdk.NewCoin("KVA", 10)},
|
||||
}
|
||||
channelKeeper.setChannel(ctx, channel)
|
||||
}
|
||||
|
||||
// create update
|
||||
// basic values
|
||||
updateToSubmit := Update{
|
||||
ChannelID: testCase.updateToSubmit.channelID,
|
||||
Payout: testCase.updateToSubmit.payout,
|
||||
// empty sig
|
||||
}
|
||||
// create update's signature
|
||||
cryptoSig, _ := privKeys[testCase.updateToSubmit.sigAccountIndex].Sign(updateToSubmit.GetSignBytes())
|
||||
updateToSubmit.Sigs = [1]UpdateSignature{UpdateSignature{
|
||||
PubKey: pubKeys[testCase.updateToSubmit.pubKeyAccountIndex],
|
||||
CryptoSignature: cryptoSig,
|
||||
}}
|
||||
|
||||
// ACTION
|
||||
_, err := channelKeeper.InitCloseChannelBySender(ctx, updateToSubmit)
|
||||
|
||||
// CHECK RESULTS
|
||||
// Check error
|
||||
if testCase.shouldError {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
// Check submittedUpdate
|
||||
su, found := channelKeeper.getSubmittedUpdate(ctx, chanID)
|
||||
switch testCase.expectedSubmittedUpdate {
|
||||
case "empty":
|
||||
assert.False(t, found)
|
||||
assert.Zero(t, su)
|
||||
case "sameAsSubmitted":
|
||||
assert.True(t, found)
|
||||
expectedSU := SubmittedUpdate{updateToSubmit, ChannelDisputeTime}
|
||||
assert.Equal(t, expectedSU, su)
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
}
|
61
internal/x/paychan/test_common.go
Normal file
61
internal/x/paychan/test_common.go
Normal file
@ -0,0 +1,61 @@
|
||||
package paychan
|
||||
|
||||
import (
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
"github.com/cosmos/cosmos-sdk/x/mock"
|
||||
//"github.com/stretchr/testify/require"
|
||||
"github.com/cosmos/cosmos-sdk/x/auth"
|
||||
"github.com/cosmos/cosmos-sdk/x/bank"
|
||||
abci "github.com/tendermint/tendermint/abci/types"
|
||||
"github.com/tendermint/tendermint/crypto"
|
||||
"github.com/tendermint/tendermint/crypto/ed25519"
|
||||
)
|
||||
|
||||
// Setup an example app with an in memory DB and the required keepers
|
||||
// Also create two accounts with 1000KVA
|
||||
// Could do with refactoring
|
||||
func createMockApp(accountSeeds []string) (sdk.Context, bank.Keeper, Keeper, []sdk.AccAddress, []crypto.PubKey, []crypto.PrivKey, sdk.Coins) {
|
||||
mApp := mock.NewApp() // creates a half complete app
|
||||
coinKeeper := bank.NewKeeper(mApp.AccountMapper)
|
||||
|
||||
// create channel keeper
|
||||
keyChannel := sdk.NewKVStoreKey("channel")
|
||||
channelKeeper := NewKeeper(mApp.Cdc, keyChannel, coinKeeper)
|
||||
// could add router for msg tests
|
||||
//mapp.Router().AddRoute("channel", NewHandler(channelKeeper))
|
||||
|
||||
mApp.CompleteSetup([]*sdk.KVStoreKey{keyChannel})
|
||||
|
||||
// create some accounts
|
||||
genAccFunding := sdk.Coins{sdk.NewCoin("KVA", 1000)}
|
||||
genAccs, addrs, pubKeys, privKeys := createTestGenAccounts(accountSeeds, genAccFunding)
|
||||
|
||||
// initialize the app with these accounts
|
||||
mock.SetGenesis(mApp, genAccs)
|
||||
|
||||
mApp.BeginBlock(abci.RequestBeginBlock{}) // going off other module tests
|
||||
ctx := mApp.BaseApp.NewContext(false, abci.Header{})
|
||||
|
||||
return ctx, coinKeeper, channelKeeper, addrs, pubKeys, privKeys, genAccFunding
|
||||
}
|
||||
|
||||
// CreateTestGenAccounts deterministically generates genesis accounts loaded with coins, and returns
|
||||
// their addresses, pubkeys, and privkeys.
|
||||
func createTestGenAccounts(accountSeeds []string, genCoins sdk.Coins) (genAccs []auth.Account, addrs []sdk.AccAddress, pubKeys []crypto.PubKey, privKeys []crypto.PrivKey) {
|
||||
for _, seed := range accountSeeds {
|
||||
privKey := ed25519.GenPrivKeyFromSecret([]byte(seed))
|
||||
pubKey := privKey.PubKey()
|
||||
addr := sdk.AccAddress(pubKey.Address())
|
||||
|
||||
genAcc := &auth.BaseAccount{
|
||||
Address: addr,
|
||||
Coins: genCoins,
|
||||
}
|
||||
|
||||
genAccs = append(genAccs, genAcc)
|
||||
privKeys = append(privKeys, privKey)
|
||||
pubKeys = append(pubKeys, pubKey)
|
||||
addrs = append(addrs, addr)
|
||||
}
|
||||
return
|
||||
}
|
220
internal/x/paychan/types.go
Normal file
220
internal/x/paychan/types.go
Normal file
@ -0,0 +1,220 @@
|
||||
package paychan
|
||||
|
||||
import (
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
"github.com/tendermint/tendermint/crypto"
|
||||
)
|
||||
|
||||
/* CHANNEL TYPES */
|
||||
|
||||
// Used to represent a channel in the keeper module.
|
||||
// Participants is limited to two as currently these are unidirectional channels.
|
||||
// Last participant is designated as receiver.
|
||||
type Channel struct {
|
||||
ID ChannelID
|
||||
Participants [2]sdk.AccAddress // [senderAddr, receiverAddr]
|
||||
Coins sdk.Coins
|
||||
}
|
||||
|
||||
const ChannelDisputeTime = int64(6) // measured in blocks TODO pick reasonable time, add to channel or genesis
|
||||
|
||||
type ChannelID int64 // TODO should this be positive only?
|
||||
|
||||
// The data that is passed between participants as payments, and submitted to the blockchain to close a channel.
|
||||
type Update struct {
|
||||
ChannelID ChannelID
|
||||
Payout Payout
|
||||
//Sequence int64 Not needed for unidirectional channels
|
||||
Sigs [1]UpdateSignature // only sender needs to sign in unidirectional
|
||||
}
|
||||
|
||||
func (u Update) GetSignBytes() []byte {
|
||||
bz, err := msgCdc.MarshalJSON(struct {
|
||||
ChannelID ChannelID
|
||||
Payout Payout
|
||||
}{
|
||||
ChannelID: u.ChannelID,
|
||||
Payout: u.Payout})
|
||||
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return sdk.MustSortJSON(bz)
|
||||
}
|
||||
|
||||
type Payout [2]sdk.Coins // a list of coins to be paid to each of Channel.Participants
|
||||
func (p Payout) IsNotNegative() bool {
|
||||
result := true
|
||||
for _, coins := range p {
|
||||
result = result && coins.IsNotNegative()
|
||||
}
|
||||
return result
|
||||
}
|
||||
func (p Payout) Sum() sdk.Coins {
|
||||
var total sdk.Coins
|
||||
for _, coins := range p {
|
||||
total = total.Plus(coins.Sort())
|
||||
total = total.Sort()
|
||||
}
|
||||
return total
|
||||
}
|
||||
|
||||
type UpdateSignature struct {
|
||||
PubKey crypto.PubKey
|
||||
CryptoSignature crypto.Signature
|
||||
}
|
||||
|
||||
// An update that has been submitted to the blockchain, but not yet acted on.
|
||||
type SubmittedUpdate struct {
|
||||
Update
|
||||
ExecutionTime int64 // BlockHeight
|
||||
}
|
||||
|
||||
type SubmittedUpdatesQueue []ChannelID // not technically a queue
|
||||
|
||||
// Check if value is in queue
|
||||
func (suq SubmittedUpdatesQueue) Contains(channelID ChannelID) bool {
|
||||
found := false
|
||||
for _, id := range suq {
|
||||
if id == channelID {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
return found
|
||||
}
|
||||
|
||||
// Remove all values from queue that match argument
|
||||
func (suq *SubmittedUpdatesQueue) RemoveMatchingElements(channelID ChannelID) {
|
||||
newSUQ := SubmittedUpdatesQueue{}
|
||||
|
||||
for _, id := range *suq {
|
||||
if id != channelID {
|
||||
newSUQ = append(newSUQ, id)
|
||||
}
|
||||
}
|
||||
*suq = newSUQ
|
||||
}
|
||||
|
||||
/* MESSAGE TYPES */
|
||||
/*
|
||||
Message implement the sdk.Msg interface:
|
||||
type Msg interface {
|
||||
|
||||
// Return the message type.
|
||||
// Must be alphanumeric or empty.
|
||||
Type() string
|
||||
|
||||
// Get the canonical byte representation of the Msg.
|
||||
GetSignBytes() []byte
|
||||
|
||||
// ValidateBasic does a simple validation check that
|
||||
// doesn't require access to any other information.
|
||||
ValidateBasic() Error
|
||||
|
||||
// Signers returns the addrs of signers that must sign.
|
||||
// CONTRACT: All signatures must be present to be valid.
|
||||
// CONTRACT: Returns addrs in some deterministic order.
|
||||
GetSigners() []AccAddress
|
||||
}
|
||||
*/
|
||||
|
||||
// A message to create a payment channel.
|
||||
type MsgCreate struct {
|
||||
Participants [2]sdk.AccAddress
|
||||
Coins sdk.Coins
|
||||
}
|
||||
|
||||
func (msg MsgCreate) Type() string { return "paychan" }
|
||||
|
||||
func (msg MsgCreate) GetSignBytes() []byte {
|
||||
bz, err := msgCdc.MarshalJSON(msg)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return sdk.MustSortJSON(bz)
|
||||
}
|
||||
|
||||
func (msg MsgCreate) ValidateBasic() sdk.Error {
|
||||
// Validate msg as an optimisation to avoid all validation going to keeper. It's run before the sigs are checked by the auth module.
|
||||
// Validate without external information (such as account balance)
|
||||
|
||||
//TODO implement
|
||||
|
||||
/* old logic
|
||||
// check if all fields present / not 0 valued
|
||||
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.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.IsPositive() {
|
||||
return sdk.ErrInvalidCoins(msg.Amount.String())
|
||||
}
|
||||
// TODO check if Address valid?
|
||||
*/
|
||||
return nil
|
||||
}
|
||||
|
||||
func (msg MsgCreate) GetSigners() []sdk.AccAddress {
|
||||
// Only sender must sign to create a paychan
|
||||
return []sdk.AccAddress{msg.Participants[0]} // select sender address
|
||||
}
|
||||
|
||||
// A message to close a payment channel.
|
||||
type MsgSubmitUpdate struct {
|
||||
Update
|
||||
Submitter sdk.AccAddress
|
||||
}
|
||||
|
||||
func (msg MsgSubmitUpdate) Type() string { return "paychan" }
|
||||
|
||||
func (msg MsgSubmitUpdate) GetSignBytes() []byte {
|
||||
bz, err := msgCdc.MarshalJSON(msg)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return sdk.MustSortJSON(bz)
|
||||
}
|
||||
|
||||
func (msg MsgSubmitUpdate) ValidateBasic() sdk.Error {
|
||||
|
||||
// TODO implement
|
||||
/* old logic
|
||||
// check if all fields present / not 0 valued
|
||||
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.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
|
||||
}
|
||||
// Check if coins are sorted, non zero, non negative
|
||||
if !msg.ReceiverAmount.IsValid() {
|
||||
return sdk.ErrInvalidCoins(msg.ReceiverAmount.String())
|
||||
}
|
||||
if !msg.ReceiverAmount.IsPositive() {
|
||||
return sdk.ErrInvalidCoins(msg.ReceiverAmount.String())
|
||||
}
|
||||
// TODO check if Address valid?
|
||||
*/
|
||||
return nil
|
||||
}
|
||||
|
||||
func (msg MsgSubmitUpdate) GetSigners() []sdk.AccAddress {
|
||||
return []sdk.AccAddress{msg.Submitter}
|
||||
}
|
45
internal/x/paychan/types_test.go
Normal file
45
internal/x/paychan/types_test.go
Normal file
@ -0,0 +1,45 @@
|
||||
package paychan
|
||||
|
||||
import (
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSubmittedUpdatesQueue(t *testing.T) {
|
||||
t.Run("RemoveMatchingElements", func(t *testing.T) {
|
||||
// SETUP
|
||||
q := SubmittedUpdatesQueue{4, 8, 23, 0, 5645657}
|
||||
// ACTION
|
||||
q.RemoveMatchingElements(23)
|
||||
// CHECK RESULTS
|
||||
expectedQ := SubmittedUpdatesQueue{4, 8, 0, 5645657}
|
||||
assert.Equal(t, expectedQ, q)
|
||||
|
||||
// SETUP
|
||||
q = SubmittedUpdatesQueue{0}
|
||||
// ACTION
|
||||
q.RemoveMatchingElements(0)
|
||||
// CHECK RESULTS
|
||||
expectedQ = SubmittedUpdatesQueue{}
|
||||
assert.Equal(t, expectedQ, q)
|
||||
})
|
||||
}
|
||||
|
||||
func TestPayout(t *testing.T) {
|
||||
t.Run("IsNotNegative", func(t *testing.T) {
|
||||
p := Payout{sdk.Coins{sdk.NewCoin("USD", 4), sdk.NewCoin("GBP", 0)}, sdk.Coins{sdk.NewCoin("USD", 129879234), sdk.NewCoin("GBP", 1)}}
|
||||
assert.True(t, p.IsNotNegative())
|
||||
|
||||
p = Payout{sdk.Coins{sdk.NewCoin("USD", -4), sdk.NewCoin("GBP", 0)}, sdk.Coins{sdk.NewCoin("USD", 129879234), sdk.NewCoin("GBP", 1)}}
|
||||
assert.False(t, p.IsNotNegative())
|
||||
})
|
||||
t.Run("Sum", func(t *testing.T) {
|
||||
p := Payout{
|
||||
sdk.Coins{sdk.NewCoin("EUR", 1), sdk.NewCoin("USD", -5)},
|
||||
sdk.Coins{sdk.NewCoin("EUR", 1), sdk.NewCoin("USD", 100), sdk.NewCoin("GBP", 1)},
|
||||
}
|
||||
expected := sdk.Coins{sdk.NewCoin("EUR", 2), sdk.NewCoin("GBP", 1), sdk.NewCoin("USD", 95)}
|
||||
assert.Equal(t, expected, p.Sum())
|
||||
})
|
||||
}
|
18
internal/x/paychan/wire.go
Normal file
18
internal/x/paychan/wire.go
Normal file
@ -0,0 +1,18 @@
|
||||
package paychan
|
||||
|
||||
import (
|
||||
"github.com/cosmos/cosmos-sdk/wire"
|
||||
)
|
||||
|
||||
func RegisterWire(cdc *wire.Codec) {
|
||||
cdc.RegisterConcrete(MsgCreate{}, "paychan/MsgCreate", nil)
|
||||
cdc.RegisterConcrete(MsgSubmitUpdate{}, "paychan/MsgSubmitUpdate", nil)
|
||||
}
|
||||
|
||||
// TODO move this to near the msg definitions?
|
||||
var msgCdc = wire.NewCodec()
|
||||
|
||||
func init() {
|
||||
wire.RegisterCrypto(msgCdc)
|
||||
RegisterWire(msgCdc)
|
||||
}
|
Loading…
Reference in New Issue
Block a user