diff --git a/cmd/kvcli/main.go b/cmd/kvcli/main.go index 48c0876b..90aa90e8 100644 --- a/cmd/kvcli/main.go +++ b/cmd/kvcli/main.go @@ -55,8 +55,9 @@ func main() { )...) rootCmd.AddCommand( - client.PostCommands( + 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), diff --git a/internal/x/paychan/README.md b/internal/x/paychan/README.md new file mode 100644 index 00000000..cad4cfea --- /dev/null +++ b/internal/x/paychan/README.md @@ -0,0 +1,15 @@ +Paychan Sketch + +Simplifications: + + - unidirectional paychans + - no top ups or partial withdrawals (only opening and closing) + - no protection against fund lock up from dissapearing receiver + + + TODO + - fill in details + - add tests + - is errors.go needed? + - is wire.go needed? + - remove simplifications \ No newline at end of file diff --git a/internal/x/paychan/client/cmd/cmd.go b/internal/x/paychan/client/cmd/cmd.go new file mode 100644 index 00000000..168be42d --- /dev/null +++ b/internal/x/paychan/client/cmd/cmd.go @@ -0,0 +1,68 @@ +package cli + +import ( + "fmt" + + "github.com/spf13/cobra" + //"github.com/cosmos/cosmos-sdk/client/context" + //sdk "github.com/cosmos/cosmos-sdk/types" + //"github.com/cosmos/cosmos-sdk/wire" + //"github.com/cosmos/cosmos-sdk/x/auth" +) + +// list of functions that return pointers to cobra commands +// No local storage needed for cli acting as a sender + +// create paychan +// close paychan +// get paychan(s) +// send paychan payment +// get balance from receiver + +// example from x/auth +/* +func GetAccountCmd(storeName string, cdc *wire.Codec, decoder auth.AccountDecoder) *cobra.Command { + return &cobra.Command{ + Use: "account [address]", + Short: "Query account balance", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + + // find the key to look up the account + addr := args[0] + + key, err := sdk.GetAccAddressBech32(addr) + if err != nil { + return err + } + + // perform query + ctx := context.NewCoreContextFromViper() + res, err := ctx.Query(auth.AddressStoreKey(key), storeName) + if err != nil { + return err + } + + // Check if account was found + if res == nil { + return sdk.ErrUnknownAddress("No account with address " + addr + + " was found in the state.\nAre you sure there has been a transaction involving it?") + } + + // decode the value + account, err := decoder(res) + if err != nil { + return err + } + + // print out whole account + output, err := wire.MarshalJSONIndent(cdc, account) + if err != nil { + return err + } + fmt.Println(string(output)) + return nil + }, + } +} +*/ diff --git a/internal/x/paychan/client/rest/channel-watcher.go b/internal/x/paychan/client/rest/channel-watcher.go new file mode 100644 index 00000000..d00c7f51 --- /dev/null +++ b/internal/x/paychan/client/rest/channel-watcher.go @@ -0,0 +1,7 @@ +package lcd + +import () + +// implement thing that polls blockchain and handles paychan disputes +// needs plugged into LCD - add a "background processes" slot in the LCD run function? +// eventually LCD evolves into paychan (network) daemon diff --git a/internal/x/paychan/client/rest/rest.go b/internal/x/paychan/client/rest/rest.go new file mode 100644 index 00000000..3565d70a --- /dev/null +++ b/internal/x/paychan/client/rest/rest.go @@ -0,0 +1,22 @@ +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 diff --git a/internal/x/paychan/handler.go b/internal/x/paychan/handler.go new file mode 100644 index 00000000..e25c68f9 --- /dev/null +++ b/internal/x/paychan/handler.go @@ -0,0 +1,33 @@ +package paychan + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + "reflect" +) + +// NewHandler returns a handler for "paychan" type messages. +func NewHandler(k Keeper) sdk.Handler { + return func(ctx sdk.Context, msg sdk.Msg) sdk.Result { + switch msg := msg.(type) { + case MsgSend: + return handleMsgSend(ctx, k, msg) + case MsgIssue: + return handleMsgIssue(ctx, k, msg) + default: + errMsg := "Unrecognized paychan Msg type: " + reflect.TypeOf(msg).Name() + return sdk.ErrUnknownRequest(errMsg).Result() + } + } +} + +// Handle CreateMsg. +func handleMsgCreate(ctx sdk.Context, k Keeper, msg MsgSend) sdk.Result { + // k.CreatePaychan(args...) + // handle erros +} + +// Handle CloseMsg. +func handleMsgClose(ctx sdk.Context, k Keeper, msg MsgIssue) sdk.Result { + // k.ClosePaychan(args...) + // handle errors +} diff --git a/internal/x/paychan/keeper.go b/internal/x/paychan/keeper.go new file mode 100644 index 00000000..058ec628 --- /dev/null +++ b/internal/x/paychan/keeper.go @@ -0,0 +1,62 @@ +package paychan + +// keeper of the paychan store +type Keeper struct { + storeKey sdk.StoreKey + cdc *wire.Codec // needed? + coinKeeper bank.Keeper + + // codespace + codespace sdk.CodespaceType // ?? +} + +func NewKeeper(cdc *wire.Codec, key sdk.StoreKey, ck bank.Keeper, codespace sdk.CodespaceType) Keeper { + keeper := Keeper{ + storeKey: key, + cdc: cdc, + coinKeeper: ck, + codespace: codespace, + } + return keeper +} + +// bunch of business logic ... + +func (keeper Keeper) GetPaychan(paychanID) Paychan { + // load from DB + // unmarshall + // return +} + +func (keeper Keeper) setPaychan(pych Paychan) sdk.Error { + // marshal + // write to db +} + +func (keeer Keeper) CreatePaychan(receiver sdkAddress, amt sdk.Coins) (Paychan, sdk.Error) { + // subtract coins from sender + // create new Paychan struct (create ID) + // save to db + + // validation: + // sender has enough coins + // receiver address exists? + // paychan doesn't exist already +} + +func (keeper Keeper) ClosePaychan() sdk.Error { + // add coins to sender + // add coins to receiver + // delete paychan from db + + // validation: + // paychan exists + // output coins are less than paychan balance + // sender and receiver addresses exist? +} + +func paychanKey(Paychan) { + // concat sender and receiver and integer ID +} + +// maybe getAllPaychans(sender sdk.address) []Paychan diff --git a/internal/x/paychan/keeper_test.go b/internal/x/paychan/keeper_test.go new file mode 100644 index 00000000..344c3148 --- /dev/null +++ b/internal/x/paychan/keeper_test.go @@ -0,0 +1,93 @@ +package paychan + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +// example from x/bank +func TestKeeper(t *testing.T) { + // ms, authKey := setupMultiStore() + + // cdc := wire.NewCodec() + // auth.RegisterBaseAccount(cdc) + + // ctx := sdk.NewContext(ms, abci.Header{}, false, nil, log.NewNopLogger()) + // accountMapper := auth.NewAccountMapper(cdc, authKey, &auth.BaseAccount{}) + // coinKeeper := NewKeeper(accountMapper) + + // addr := sdk.Address([]byte("addr1")) + // addr2 := sdk.Address([]byte("addr2")) + // addr3 := sdk.Address([]byte("addr3")) + // acc := accountMapper.NewAccountWithAddress(ctx, addr) + + // // Test GetCoins/SetCoins + // accountMapper.SetAccount(ctx, acc) + // assert.True(t, coinKeeper.GetCoins(ctx, addr).IsEqual(sdk.Coins{})) + + // coinKeeper.SetCoins(ctx, addr, sdk.Coins{{"foocoin", 10}}) + // assert.True(t, coinKeeper.GetCoins(ctx, addr).IsEqual(sdk.Coins{{"foocoin", 10}})) + + // // Test HasCoins + // assert.True(t, coinKeeper.HasCoins(ctx, addr, sdk.Coins{{"foocoin", 10}})) + // assert.True(t, coinKeeper.HasCoins(ctx, addr, sdk.Coins{{"foocoin", 5}})) + // assert.False(t, coinKeeper.HasCoins(ctx, addr, sdk.Coins{{"foocoin", 15}})) + // assert.False(t, coinKeeper.HasCoins(ctx, addr, sdk.Coins{{"barcoin", 5}})) + + // // Test AddCoins + // coinKeeper.AddCoins(ctx, addr, sdk.Coins{{"foocoin", 15}}) + // assert.True(t, coinKeeper.GetCoins(ctx, addr).IsEqual(sdk.Coins{{"foocoin", 25}})) + + // coinKeeper.AddCoins(ctx, addr, sdk.Coins{{"barcoin", 15}}) + // assert.True(t, coinKeeper.GetCoins(ctx, addr).IsEqual(sdk.Coins{{"barcoin", 15}, {"foocoin", 25}})) + + // // Test SubtractCoins + // coinKeeper.SubtractCoins(ctx, addr, sdk.Coins{{"foocoin", 10}}) + // coinKeeper.SubtractCoins(ctx, addr, sdk.Coins{{"barcoin", 5}}) + // assert.True(t, coinKeeper.GetCoins(ctx, addr).IsEqual(sdk.Coins{{"barcoin", 10}, {"foocoin", 15}})) + + // coinKeeper.SubtractCoins(ctx, addr, sdk.Coins{{"barcoin", 11}}) + // assert.True(t, coinKeeper.GetCoins(ctx, addr).IsEqual(sdk.Coins{{"barcoin", 10}, {"foocoin", 15}})) + + // coinKeeper.SubtractCoins(ctx, addr, sdk.Coins{{"barcoin", 10}}) + // assert.True(t, coinKeeper.GetCoins(ctx, addr).IsEqual(sdk.Coins{{"foocoin", 15}})) + // assert.False(t, coinKeeper.HasCoins(ctx, addr, sdk.Coins{{"barcoin", 1}})) + + // // Test SendCoins + // coinKeeper.SendCoins(ctx, addr, addr2, sdk.Coins{{"foocoin", 5}}) + // assert.True(t, coinKeeper.GetCoins(ctx, addr).IsEqual(sdk.Coins{{"foocoin", 10}})) + // assert.True(t, coinKeeper.GetCoins(ctx, addr2).IsEqual(sdk.Coins{{"foocoin", 5}})) + + // _, err2 := coinKeeper.SendCoins(ctx, addr, addr2, sdk.Coins{{"foocoin", 50}}) + // assert.Implements(t, (*sdk.Error)(nil), err2) + // assert.True(t, coinKeeper.GetCoins(ctx, addr).IsEqual(sdk.Coins{{"foocoin", 10}})) + // assert.True(t, coinKeeper.GetCoins(ctx, addr2).IsEqual(sdk.Coins{{"foocoin", 5}})) + + // coinKeeper.AddCoins(ctx, addr, sdk.Coins{{"barcoin", 30}}) + // coinKeeper.SendCoins(ctx, addr, addr2, sdk.Coins{{"barcoin", 10}, {"foocoin", 5}}) + // assert.True(t, coinKeeper.GetCoins(ctx, addr).IsEqual(sdk.Coins{{"barcoin", 20}, {"foocoin", 5}})) + // assert.True(t, coinKeeper.GetCoins(ctx, addr2).IsEqual(sdk.Coins{{"barcoin", 10}, {"foocoin", 10}})) + + // // Test InputOutputCoins + // input1 := NewInput(addr2, sdk.Coins{{"foocoin", 2}}) + // output1 := NewOutput(addr, sdk.Coins{{"foocoin", 2}}) + // coinKeeper.InputOutputCoins(ctx, []Input{input1}, []Output{output1}) + // assert.True(t, coinKeeper.GetCoins(ctx, addr).IsEqual(sdk.Coins{{"barcoin", 20}, {"foocoin", 7}})) + // assert.True(t, coinKeeper.GetCoins(ctx, addr2).IsEqual(sdk.Coins{{"barcoin", 10}, {"foocoin", 8}})) + + // inputs := []Input{ + // NewInput(addr, sdk.Coins{{"foocoin", 3}}), + // NewInput(addr2, sdk.Coins{{"barcoin", 3}, {"foocoin", 2}}), + // } + + // outputs := []Output{ + // NewOutput(addr, sdk.Coins{{"barcoin", 1}}), + // NewOutput(addr3, sdk.Coins{{"barcoin", 2}, {"foocoin", 5}}), + // } + // coinKeeper.InputOutputCoins(ctx, inputs, outputs) + // assert.True(t, coinKeeper.GetCoins(ctx, addr).IsEqual(sdk.Coins{{"barcoin", 21}, {"foocoin", 4}})) + // assert.True(t, coinKeeper.GetCoins(ctx, addr2).IsEqual(sdk.Coins{{"barcoin", 7}, {"foocoin", 6}})) + // assert.True(t, coinKeeper.GetCoins(ctx, addr3).IsEqual(sdk.Coins{{"barcoin", 2}, {"foocoin", 5}})) + +} diff --git a/internal/x/paychan/types.go b/internal/x/paychan/types.go new file mode 100644 index 00000000..aec54ebc --- /dev/null +++ b/internal/x/paychan/types.go @@ -0,0 +1,98 @@ +package paychan + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" +) + +//////// + +// probably want to convert this to a general purpose "state" +struct Paychan { + balance sdk.Coins + sender sdk.Address + receiver sdk.Address +} + + +///////////// + +// 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() []Address +// } + +/////////////// CreatePayChan +// find a less confusing name +type CreateMsg struct { + // maybe just wrap a paychan struct + sender sdk.Address + receiver sdk.Address + amount sdk.Balance +} + +func (msg CreatMsg) NewCreateMsg() CreateMsg { + return CreateMsg{ } +} + +func (msg CreateMsg) Type() string { return "paychan" } + +func (msg CreateMsg) GetSigners() []sdk.Address { + // sender + //return []sdk.Address{msg.sender} +} + +func (msg CreateMsg) GetSignBytes() []byte { + +} + +func (msg CreateMsg) ValidateBasic() sdk.Error { + // verify msg as much as possible without using external information (such as account balance) + // are all fields present + // are all fields valid + // maybe check if sender and receiver is different +} + +///////////////// +type CloseMsg 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 integer + receiverAmount sdk.Coins // amount the receiver should get - sender amount implicit with paychan balance +} + +func (msg CloseMsg) NewCloseMsg( args... ) CloseMsg { + return CloseMsg{ args... } +} + +func (msg CloseMsg) Type() string { return "paychan" } + +func (msg CloseMsg) GetSigners() []sdk.Address { + // sender and receiver +} + +func (msg CloseMsg) GetSignBytes() []byte { + +} + +func (msg CloseMsg) ValidateBasic() sdk.Error { + return msg.IBCPacket.ValidateBasic() +} +