diff --git a/.gitignore b/.gitignore index 707ded550..b0684f207 100644 --- a/.gitignore +++ b/.gitignore @@ -1,22 +1,23 @@ +# OS +.DS_Store *.swp *.swo +.vscode + +# Build vendor -merkleeyes.db build -docs/guide/*.sh tools/bin/* examples/build/* -examples/basecoin/glide.lock +docs/_build + +# Data - ideally these don't exist examples/basecoin/app/data baseapp/data/* -docs/_build -.DS_Store + +# Testing coverage.txt profile.out -.vscode -coverage.txt -profile.out -client/lcd/keys.db/ ### Vagrant ### .vagrant/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 01d342ef6..03ef49d35 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,37 @@ # Changelog +## 0.13.0 (April 2, 2018) + +BREAKING CHANGES + +* [basecoin] Remove cool/sketchy modules -> moved to new `democoin` +* [basecoin] NewBasecoinApp takes a `map[string]dbm.DB` as temporary measure + to allow mounting multiple stores with their own DB until they can share one +* [staking] Renamed to `simplestake` +* [builder] Functions don't take `passphrase` as argument +* [server] GenAppState returns generated seed and address +* [basecoind] `init` command outputs JSON of everything necessary for testnet +* [basecoind] `basecoin.db -> data/basecoin.db` +* [basecli] `data/keys.db -> keys/keys.db` + +FEATURES + +* [types] `Coin` supports direct arithmetic operations +* [basecoind] Add `show_validator` and `show_node_id` commands +* [staking] Initial merge of full staking module! +* [democoin] New example application to demo custom modules + +IMPROVEMENTS + +* [makefile] `make install` +* [testing] Use `/tmp` for directories so they don't get left in the repo + +BUG FIXES + +* [basecoin] Allow app to be restarted +* [makefile] Fix build on Windows +* [basecli] Get confirmation before overriding key with same name + ## 0.12.0 (March 27 2018) BREAKING CHANGES @@ -26,7 +58,7 @@ FEATURES * [types] StdFee, and StdTx takes the StdFee * [specs] Progression of MVPs for IBC * [x/ibc] Initial shell of IBC functionality (no proofs) -* [x/staking] Simple staking module with bonding/unbonding +* [x/simplestake] Simple staking module with bonding/unbonding IMPROVEMENTS diff --git a/Gopkg.lock b/Gopkg.lock index 46cb95893..377390297 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -458,6 +458,6 @@ [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "0eb39694057c8ab8c9ecbaeb25bc43cbf1d2422976a09a67392a62dcef149a7b" + inputs-digest = "ed1f3f7f1728cd02945f90ca780e9bdc982573a36a5cc8d7e9f19fb40ba2ca19" solver-name = "gps-cdcl" solver-version = 1 diff --git a/Makefile b/Makefile index 447d051c0..023e4f8c6 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,6 @@ PACKAGES=$(shell go list ./... | grep -v '/vendor/') -BUILD_FLAGS = -ldflags "-X github.com/cosmos/cosmos-sdk/version.GitCommit=`git rev-parse --short HEAD`" +COMMIT_HASH := $(shell git rev-parse --short HEAD) +BUILD_FLAGS = -ldflags "-X github.com/cosmos/cosmos-sdk/version.GitCommit=${COMMIT_HASH}" all: check_tools get_vendor_deps build test @@ -17,9 +18,25 @@ gaia: go build $(BUILD_FLAGS) -o build/gaiacli ./examples/gaia/gaiacli build: - @rm -rf examples/basecoin/vendor/ + @rm -rf $(shell pwd)/examples/basecoin/vendor/ + @rm -rf $(shell pwd)/examples/democoin/vendor/ +ifeq ($(OS),Windows_NT) + go build $(BUILD_FLAGS) -o build/basecoind.exe ./examples/basecoin/cmd/basecoind + go build $(BUILD_FLAGS) -o build/basecli.exe ./examples/basecoin/cmd/basecli + go build $(BUILD_FLAGS) -o build/democoind.exe ./examples/democoin/cmd/democoind + go build $(BUILD_FLAGS) -o build/democli.exe ./examples/democoin/cmd/democli +else go build $(BUILD_FLAGS) -o build/basecoind ./examples/basecoin/cmd/basecoind go build $(BUILD_FLAGS) -o build/basecli ./examples/basecoin/cmd/basecli + go build $(BUILD_FLAGS) -o build/democoind ./examples/democoin/cmd/democoind + go build $(BUILD_FLAGS) -o build/democli ./examples/democoin/cmd/democli +endif + +install: + go install $(BUILD_FLAGS) ./examples/basecoin/cmd/basecoind + go install $(BUILD_FLAGS) ./examples/basecoin/cmd/basecli + go install $(BUILD_FLAGS) ./examples/democoin/cmd/democoind + go install $(BUILD_FLAGS) ./examples/democoin/cmd/democli dist: @bash publish/dist.sh @@ -68,13 +85,12 @@ test: test_unit # test_cli test_unit: @rm -rf examples/basecoin/vendor/ + @rm -rf examples/democoin/vendor/ @go test $(PACKAGES) test_cover: @rm -rf examples/basecoin/vendor/ - @rm -rf client/lcd/keys.db ~/.tendermint_test @bash tests/test_cover.sh - @rm -rf client/lcd/keys.db ~/.tendermint_test benchmark: @go test -bench=. $(PACKAGES) diff --git a/baseapp/baseapp.go b/baseapp/baseapp.go index 3dad0483a..e0ef39cde 100644 --- a/baseapp/baseapp.go +++ b/baseapp/baseapp.go @@ -1,6 +1,7 @@ package baseapp import ( + "encoding/json" "fmt" "runtime/debug" @@ -54,6 +55,7 @@ type BaseApp struct { var _ abci.Application = (*BaseApp)(nil) // Create and name new BaseApp +// NOTE: The db is used to store the version number for now. func NewBaseApp(name string, logger log.Logger, db dbm.DB) *BaseApp { return &BaseApp{ Logger: logger, @@ -70,12 +72,18 @@ func (app *BaseApp) Name() string { } // Mount a store to the provided key in the BaseApp multistore +// Broken until #532 is implemented. func (app *BaseApp) MountStoresIAVL(keys ...*sdk.KVStoreKey) { for _, key := range keys { app.MountStore(key, sdk.StoreTypeIAVL) } } +// Mount a store to the provided key in the BaseApp multistore +func (app *BaseApp) MountStoreWithDB(key sdk.StoreKey, typ sdk.StoreType, db dbm.DB) { + app.cms.MountStoreWithDB(key, typ, db) +} + // Mount a store to the provided key in the BaseApp multistore func (app *BaseApp) MountStore(key sdk.StoreKey, typ sdk.StoreType) { app.cms.MountStoreWithDB(key, typ, app.db) @@ -233,6 +241,14 @@ func (app *BaseApp) InitChain(req abci.RequestInitChain) (res abci.ResponseInitC app.setDeliverState(abci.Header{}) app.initChainer(app.deliverState.ctx, req) // no error + // Initialize module genesis state + genesisState := new(map[string]json.RawMessage) + err := json.Unmarshal(req.AppStateBytes, genesisState) + if err != nil { + // TODO Return something intelligent + panic(err) + } + // NOTE: we don't commit, but BeginBlock for block 1 // starts from this deliverState diff --git a/baseapp/baseapp_test.go b/baseapp/baseapp_test.go index de9a0253c..1658c591d 100644 --- a/baseapp/baseapp_test.go +++ b/baseapp/baseapp_test.go @@ -35,12 +35,15 @@ func TestMountStores(t *testing.T) { // make some cap keys capKey1 := sdk.NewKVStoreKey("key1") + db1 := dbm.NewMemDB() capKey2 := sdk.NewKVStoreKey("key2") + db2 := dbm.NewMemDB() // no stores are mounted assert.Panics(t, func() { app.LoadLatestVersion(capKey1) }) - app.MountStoresIAVL(capKey1, capKey2) + app.MountStoreWithDB(capKey1, sdk.StoreTypeIAVL, db1) + app.MountStoreWithDB(capKey2, sdk.StoreTypeIAVL, db2) // stores are mounted err := app.LoadLatestVersion(capKey1) @@ -126,7 +129,6 @@ func TestTxDecoder(t *testing.T) { // Test that Info returns the latest committed state. func TestInfo(t *testing.T) { - app := newBaseApp(t.Name()) // ----- test an empty response ------- @@ -145,17 +147,19 @@ func TestInfo(t *testing.T) { } func TestInitChainer(t *testing.T) { - logger := defaultLogger() - db := dbm.NewMemDB() name := t.Name() + db := dbm.NewMemDB() + logger := defaultLogger() app := NewBaseApp(name, logger, db) - // make cap keys and mount the stores // NOTE/TODO: mounting multiple stores is broken // see https://github.com/cosmos/cosmos-sdk/issues/532 capKey := sdk.NewKVStoreKey("main") - // capKey2 := sdk.NewKVStoreKey("key2") - app.MountStoresIAVL(capKey) // , capKey2) + db1 := dbm.NewMemDB() + capKey2 := sdk.NewKVStoreKey("key2") + db2 := dbm.NewMemDB() + app.MountStoreWithDB(capKey, sdk.StoreTypeIAVL, db1) + app.MountStoreWithDB(capKey2, sdk.StoreTypeIAVL, db2) err := app.LoadLatestVersion(capKey) // needed to make stores non-nil assert.Nil(t, err) @@ -180,16 +184,15 @@ func TestInitChainer(t *testing.T) { // set initChainer and try again - should see the value app.SetInitChainer(initChainer) - app.InitChain(abci.RequestInitChain{}) + app.InitChain(abci.RequestInitChain{AppStateBytes: []byte("{}")}) // must have valid JSON genesis file, even if empty app.Commit() res = app.Query(query) assert.Equal(t, value, res.Value) // reload app app = NewBaseApp(name, logger, db) - capKey = sdk.NewKVStoreKey("main") - // capKey2 = sdk.NewKVStoreKey("key2") // TODO - app.MountStoresIAVL(capKey) //, capKey2) + app.MountStoreWithDB(capKey, sdk.StoreTypeIAVL, db1) + app.MountStoreWithDB(capKey2, sdk.StoreTypeIAVL, db2) err = app.LoadLatestVersion(capKey) // needed to make stores non-nil assert.Nil(t, err) app.SetInitChainer(initChainer) diff --git a/baseapp/router.go b/baseapp/router.go index a9d754716..83efe5dad 100644 --- a/baseapp/router.go +++ b/baseapp/router.go @@ -12,7 +12,7 @@ type Router interface { Route(path string) (h sdk.Handler) } -// map a transaction type to a handler +// map a transaction type to a handler and an initgenesis function type route struct { r string h sdk.Handler diff --git a/client/builder/builder.go b/client/builder/builder.go index 2fb1c824e..ce8ad0495 100644 --- a/client/builder/builder.go +++ b/client/builder/builder.go @@ -124,7 +124,12 @@ func SignAndBuild(name, passphrase string, msg sdk.Msg, cdc *wire.Codec) ([]byte } // sign and build the transaction from the msg -func SignBuildBroadcast(name string, passphrase string, msg sdk.Msg, cdc *wire.Codec) (*ctypes.ResultBroadcastTxCommit, error) { +func SignBuildBroadcast(name string, msg sdk.Msg, cdc *wire.Codec) (*ctypes.ResultBroadcastTxCommit, error) { + passphrase, err := GetPassphraseFromStdin(name) + if err != nil { + return nil, err + } + txBytes, err := SignAndBuild(name, passphrase, msg, cdc) if err != nil { return nil, err @@ -132,3 +137,10 @@ func SignBuildBroadcast(name string, passphrase string, msg sdk.Msg, cdc *wire.C return BroadcastTx(txBytes) } + +// get passphrase from std input +func GetPassphraseFromStdin(name string) (pass string, err error) { + buf := client.BufferStdin() + prompt := fmt.Sprintf("Password to sign with '%s':", name) + return client.GetPassword(prompt, buf) +} diff --git a/client/input.go b/client/input.go index 6036b68f9..53906ca88 100644 --- a/client/input.go +++ b/client/input.go @@ -73,6 +73,28 @@ func GetCheckPassword(prompt, prompt2 string, buf *bufio.Reader) (string, error) return pass, nil } +// GetConfirmation will request user give the confirmation from stdin. +// "y", "Y", "yes", "YES", and "Yes" all count as confirmations. +// If the input is not recognized, it will ask again. +func GetConfirmation(prompt string, buf *bufio.Reader) (bool, error) { + for { + if inputIsTty() { + fmt.Print(fmt.Sprintf("%s [y/n]:", prompt)) + } + response, err := readLineFromBuf(buf) + if err != nil { + return false, err + } + + response = strings.ToLower(strings.TrimSpace(response)) + if response == "y" || response == "yes" { + return true, nil + } else if response == "n" || response == "no" { + return false, nil + } + } +} + // inputIsTty returns true iff we have an interactive prompt, // where we can disable echo and request to repeat the password. // If false, we can optimize for piped input from another command diff --git a/client/keys/add.go b/client/keys/add.go index 356c7369c..d68983028 100644 --- a/client/keys/add.go +++ b/client/keys/add.go @@ -60,6 +60,16 @@ func runAddCmd(cmd *cobra.Command, args []string) error { if err != nil { return err } + + _, err := kb.Get(name) + if err == nil { + // account exists, ask for user confirmation + if response, err := client.GetConfirmation( + fmt.Sprintf("override the existing name %s", name), buf); err != nil || !response { + return err + } + } + pass, err = client.GetCheckPassword( "Enter a passphrase for your key:", "Repeat the passphrase:", buf) diff --git a/client/keys/utils.go b/client/keys/utils.go index c6239002c..d1b3d3f65 100644 --- a/client/keys/utils.go +++ b/client/keys/utils.go @@ -2,6 +2,7 @@ package keys import ( "fmt" + "path/filepath" "github.com/spf13/viper" @@ -32,7 +33,7 @@ type KeyOutput struct { func GetKeyBase() (keys.Keybase, error) { if keybase == nil { rootDir := viper.GetString(cli.HomeFlag) - db, err := dbm.NewGoLevelDB(KeyDBName, rootDir) + db, err := dbm.NewGoLevelDB(KeyDBName, filepath.Join(rootDir, "keys")) if err != nil { return nil, err } diff --git a/client/lcd/.gitignore b/client/lcd/.gitignore deleted file mode 100644 index ae9067025..000000000 --- a/client/lcd/.gitignore +++ /dev/null @@ -1 +0,0 @@ -tmp-base* \ No newline at end of file diff --git a/client/lcd/lcd_test.go b/client/lcd/lcd_test.go index 285cf4d6d..48cc35a8e 100644 --- a/client/lcd/lcd_test.go +++ b/client/lcd/lcd_test.go @@ -25,6 +25,7 @@ import ( ctypes "github.com/tendermint/tendermint/rpc/core/types" tmrpc "github.com/tendermint/tendermint/rpc/lib/server" tmtypes "github.com/tendermint/tendermint/types" + "github.com/tendermint/tmlibs/cli" dbm "github.com/tendermint/tmlibs/db" "github.com/tendermint/tmlibs/log" @@ -157,7 +158,7 @@ func TestNodeStatus(t *testing.T) { func TestBlock(t *testing.T) { - time.Sleep(time.Second * 2) // TODO: LOL -> wait for blocks + waitForHeight(2) var resultBlock ctypes.ResultBlock @@ -221,8 +222,7 @@ func TestCoinSend(t *testing.T) { // create TX receiveAddr, resultTx := doSend(t, port, seed) - - time.Sleep(time.Second * 2) // T + waitForHeight(resultTx.Height + 1) // check if tx was commited assert.Equal(t, uint32(0), resultTx.CheckTx.Code) @@ -252,6 +252,32 @@ func TestCoinSend(t *testing.T) { assert.Equal(t, int64(1), mycoins.Amount) } +func TestIBCTransfer(t *testing.T) { + + // create TX + resultTx := doIBCTransfer(t, port, seed) + + waitForHeight(resultTx.Height + 1) + + // check if tx was commited + assert.Equal(t, uint32(0), resultTx.CheckTx.Code) + assert.Equal(t, uint32(0), resultTx.DeliverTx.Code) + + // query sender + res, body := request(t, port, "GET", "/accounts/"+sendAddr, nil) + require.Equal(t, http.StatusOK, res.StatusCode, body) + + var m auth.BaseAccount + err := json.Unmarshal([]byte(body), &m) + require.Nil(t, err) + coins := m.Coins + mycoins := coins[0] + assert.Equal(t, coinDenom, mycoins.Denom) + assert.Equal(t, coinAmount-2, mycoins.Amount) + + // TODO: query ibc egress packet state +} + func TestTxs(t *testing.T) { // TODO: re-enable once we can get txs by tag @@ -269,7 +295,7 @@ func TestTxs(t *testing.T) { // create TX _, resultTx := doSend(t, port, seed) - time.Sleep(time.Second * 2) // TO + waitForHeight(resultTx.Height + 1) // check if tx is findable res, body := request(t, port, "GET", fmt.Sprintf("/txs/%s", resultTx.Hash), nil) @@ -294,6 +320,7 @@ func TestTxs(t *testing.T) { // strt TM and the LCD in process, listening on their respective sockets func startTMAndLCD() (*nm.Node, net.Listener, error) { + viper.Set(cli.HomeFlag, os.TempDir()) kb, err := keys.GetKeyBase() // dbm.NewMemDB()) // :( if err != nil { return nil, nil, err @@ -315,7 +342,13 @@ func startTMAndLCD() (*nm.Node, net.Listener, error) { logger = log.NewFilter(logger, log.AllowError()) privValidatorFile := config.PrivValidatorFile() privVal := tmtypes.LoadOrGenPrivValidatorFS(privValidatorFile) - app := bapp.NewBasecoinApp(logger, dbm.NewMemDB()) + dbs := map[string]dbm.DB{ + "main": dbm.NewMemDB(), + "acc": dbm.NewMemDB(), + "ibc": dbm.NewMemDB(), + "staking": dbm.NewMemDB(), + } + app := bapp.NewBasecoinApp(logger, dbs) genesisFile := config.GenesisFile() genDoc, err := tmtypes.GenesisDocFromFile(genesisFile) @@ -324,8 +357,8 @@ func startTMAndLCD() (*nm.Node, net.Listener, error) { } coins := sdk.Coins{{coinDenom, coinAmount}} - appState := btypes.GenesisState{ - Accounts: []*btypes.GenesisAccount{ + appState := map[string]interface{}{ + "accounts": []*btypes.GenesisAccount{ { Name: "tester", Address: pubKey.Address(), @@ -358,7 +391,7 @@ func startTMAndLCD() (*nm.Node, net.Listener, error) { return nil, nil, err } - time.Sleep(time.Second * 2) + waitForStart() return node, lcd, nil } @@ -408,6 +441,7 @@ func request(t *testing.T, port, method, path string, payload []byte) (*http.Res require.Nil(t, err) output, err := ioutil.ReadAll(res.Body) + res.Body.Close() require.Nil(t, err) return res, string(output) @@ -427,8 +461,6 @@ func doSend(t *testing.T, port, seed string) (receiveAddr string, resultTx ctype acc := auth.BaseAccount{} err = json.Unmarshal([]byte(body), &acc) require.Nil(t, err) - fmt.Println("BODY", body) - fmt.Println("ACC", acc) sequence := acc.Sequence // send @@ -441,3 +473,99 @@ func doSend(t *testing.T, port, seed string) (receiveAddr string, resultTx ctype return receiveAddr, resultTx } + +func doIBCTransfer(t *testing.T, port, seed string) (resultTx ctypes.ResultBroadcastTxCommit) { + + // create receive address + kb := client.MockKeyBase() + receiveInfo, _, err := kb.Create("receive_address", "1234567890", cryptoKeys.CryptoAlgo("ed25519")) + require.Nil(t, err) + receiveAddr := receiveInfo.PubKey.Address().String() + + // get the account to get the sequence + res, body := request(t, port, "GET", "/accounts/"+sendAddr, nil) + // require.Equal(t, http.StatusOK, res.StatusCode, body) + acc := auth.BaseAccount{} + err = json.Unmarshal([]byte(body), &acc) + require.Nil(t, err) + sequence := acc.Sequence + + // send + jsonStr := []byte(fmt.Sprintf(`{ "name":"%s", "password":"%s", "sequence":%d, "amount":[{ "denom": "%s", "amount": 1 }] }`, name, password, sequence, coinDenom)) + res, body = request(t, port, "POST", "/ibc/testchain/"+receiveAddr+"/send", jsonStr) + require.Equal(t, http.StatusOK, res.StatusCode, body) + + err = json.Unmarshal([]byte(body), &resultTx) + require.Nil(t, err) + + return resultTx +} + +func waitForHeight(height int64) { + for { + var resultBlock ctypes.ResultBlock + + url := fmt.Sprintf("http://localhost:%v%v", port, "/blocks/latest") + res, err := http.Get(url) + if err != nil { + panic(err) + } + + body, err := ioutil.ReadAll(res.Body) + if err != nil { + panic(err) + } + res.Body.Close() + + err = json.Unmarshal([]byte(body), &resultBlock) + if err != nil { + fmt.Println("RES", res) + fmt.Println("BODY", string(body)) + panic(err) + } + + if resultBlock.Block.Height >= height { + return + } + time.Sleep(time.Millisecond * 100) + } +} + +// wait for 2 blocks +func waitForStart() { + waitHeight := int64(2) + for { + time.Sleep(time.Second) + + var resultBlock ctypes.ResultBlock + + url := fmt.Sprintf("http://localhost:%v%v", port, "/blocks/latest") + res, err := http.Get(url) + if err != nil { + panic(err) + } + + // waiting for server to start ... + if res.StatusCode != http.StatusOK { + res.Body.Close() + continue + } + + body, err := ioutil.ReadAll(res.Body) + if err != nil { + panic(err) + } + res.Body.Close() + + err = json.Unmarshal([]byte(body), &resultBlock) + if err != nil { + fmt.Println("RES", res) + fmt.Println("BODY", string(body)) + panic(err) + } + + if resultBlock.Block.Height >= waitHeight { + return + } + } +} diff --git a/client/lcd/root.go b/client/lcd/root.go index 7f18af59d..9464081e0 100644 --- a/client/lcd/root.go +++ b/client/lcd/root.go @@ -20,6 +20,7 @@ import ( "github.com/cosmos/cosmos-sdk/wire" auth "github.com/cosmos/cosmos-sdk/x/auth/rest" bank "github.com/cosmos/cosmos-sdk/x/bank/rest" + ibc "github.com/cosmos/cosmos-sdk/x/ibc/rest" ) const ( @@ -78,5 +79,6 @@ func createHandler(cdc *wire.Codec) http.Handler { tx.RegisterRoutes(r, cdc) auth.RegisterRoutes(r, cdc, "main") bank.RegisterRoutes(r, cdc, kb) + ibc.RegisterRoutes(r, cdc, kb) return r } diff --git a/client/rpc/validators.go b/client/rpc/validators.go index 32c7680ec..15c3230e3 100644 --- a/client/rpc/validators.go +++ b/client/rpc/validators.go @@ -24,7 +24,7 @@ func validatorCommand() *cobra.Command { return cmd } -func getValidators(height *int64) ([]byte, error) { +func GetValidators(height *int64) ([]byte, error) { // get the node node, err := client.GetNode() if err != nil { @@ -59,7 +59,7 @@ func printValidators(cmd *cobra.Command, args []string) error { } } - output, err := getValidators(height) + output, err := GetValidators(height) if err != nil { return err } @@ -84,7 +84,7 @@ func ValidatorsetRequestHandler(w http.ResponseWriter, r *http.Request) { w.Write([]byte("ERROR: Requested block height is bigger then the chain length.")) return } - output, err := getValidators(&height) + output, err := GetValidators(&height) if err != nil { w.WriteHeader(500) w.Write([]byte(err.Error())) @@ -100,7 +100,7 @@ func LatestValidatorsetRequestHandler(w http.ResponseWriter, r *http.Request) { w.Write([]byte(err.Error())) return } - output, err := getValidators(&height) + output, err := GetValidators(&height) if err != nil { w.WriteHeader(500) w.Write([]byte(err.Error())) diff --git a/examples/basecoin/app/app.go b/examples/basecoin/app/app.go index 1d31b0edc..caae16846 100644 --- a/examples/basecoin/app/app.go +++ b/examples/basecoin/app/app.go @@ -15,11 +15,9 @@ import ( "github.com/cosmos/cosmos-sdk/x/auth" "github.com/cosmos/cosmos-sdk/x/bank" "github.com/cosmos/cosmos-sdk/x/ibc" - "github.com/cosmos/cosmos-sdk/x/staking" + "github.com/cosmos/cosmos-sdk/x/simplestake" "github.com/cosmos/cosmos-sdk/examples/basecoin/types" - "github.com/cosmos/cosmos-sdk/examples/basecoin/x/cool" - "github.com/cosmos/cosmos-sdk/examples/basecoin/x/sketchy" ) const ( @@ -33,6 +31,7 @@ type BasecoinApp struct { // keys to access the substores capKeyMainStore *sdk.KVStoreKey + capKeyAccountStore *sdk.KVStoreKey capKeyIBCStore *sdk.KVStoreKey capKeyStakingStore *sdk.KVStoreKey @@ -40,14 +39,15 @@ type BasecoinApp struct { accountMapper sdk.AccountMapper } -func NewBasecoinApp(logger log.Logger, db dbm.DB) *BasecoinApp { +func NewBasecoinApp(logger log.Logger, dbs map[string]dbm.DB) *BasecoinApp { // create your application object var app = &BasecoinApp{ - BaseApp: bam.NewBaseApp(appName, logger, db), + BaseApp: bam.NewBaseApp(appName, logger, dbs["main"]), cdc: MakeCodec(), capKeyMainStore: sdk.NewKVStoreKey("main"), + capKeyAccountStore: sdk.NewKVStoreKey("acc"), capKeyIBCStore: sdk.NewKVStoreKey("ibc"), - capKeyStakingStore: sdk.NewKVStoreKey("staking"), + capKeyStakingStore: sdk.NewKVStoreKey("stake"), } // define the accountMapper @@ -58,20 +58,22 @@ func NewBasecoinApp(logger log.Logger, db dbm.DB) *BasecoinApp { // add handlers coinKeeper := bank.NewCoinKeeper(app.accountMapper) - coolMapper := cool.NewMapper(app.capKeyMainStore) ibcMapper := ibc.NewIBCMapper(app.cdc, app.capKeyIBCStore) - stakingMapper := staking.NewMapper(app.capKeyStakingStore) + stakeKeeper := simplestake.NewKeeper(app.capKeyStakingStore, coinKeeper) app.Router(). AddRoute("bank", bank.NewHandler(coinKeeper)). - AddRoute("cool", cool.NewHandler(coinKeeper, coolMapper)). - AddRoute("sketchy", sketchy.NewHandler()). AddRoute("ibc", ibc.NewHandler(ibcMapper, coinKeeper)). - AddRoute("staking", staking.NewHandler(stakingMapper, coinKeeper)) + AddRoute("simplestake", simplestake.NewHandler(stakeKeeper)) // initialize BaseApp app.SetTxDecoder(app.txDecoder) app.SetInitChainer(app.initChainer) - app.MountStoresIAVL(app.capKeyMainStore, app.capKeyIBCStore, app.capKeyStakingStore) + app.MountStoreWithDB(app.capKeyMainStore, sdk.StoreTypeIAVL, dbs["main"]) + app.MountStoreWithDB(app.capKeyAccountStore, sdk.StoreTypeIAVL, dbs["acc"]) + app.MountStoreWithDB(app.capKeyIBCStore, sdk.StoreTypeIAVL, dbs["ibc"]) + app.MountStoreWithDB(app.capKeyStakingStore, sdk.StoreTypeIAVL, dbs["staking"]) + // NOTE: Broken until #532 lands + //app.MountStoresIAVL(app.capKeyMainStore, app.capKeyIBCStore, app.capKeyStakingStore) app.SetAnteHandler(auth.NewAnteHandler(app.accountMapper)) err := app.LoadLatestVersion(app.capKeyMainStore) if err != nil { @@ -96,12 +98,10 @@ func MakeCodec() *wire.Codec { struct{ sdk.Msg }{}, oldwire.ConcreteType{bank.SendMsg{}, msgTypeSend}, oldwire.ConcreteType{bank.IssueMsg{}, msgTypeIssue}, - oldwire.ConcreteType{cool.QuizMsg{}, msgTypeQuiz}, - oldwire.ConcreteType{cool.SetTrendMsg{}, msgTypeSetTrend}, oldwire.ConcreteType{ibc.IBCTransferMsg{}, msgTypeIBCTransferMsg}, oldwire.ConcreteType{ibc.IBCReceiveMsg{}, msgTypeIBCReceiveMsg}, - oldwire.ConcreteType{staking.BondMsg{}, msgTypeBondMsg}, - oldwire.ConcreteType{staking.UnbondMsg{}, msgTypeUnbondMsg}, + oldwire.ConcreteType{simplestake.BondMsg{}, msgTypeBondMsg}, + oldwire.ConcreteType{simplestake.UnbondMsg{}, msgTypeUnbondMsg}, ) const accTypeApp = 0x1 diff --git a/examples/basecoin/app/app_test.go b/examples/basecoin/app/app_test.go index f97ae30bf..4958240b5 100644 --- a/examples/basecoin/app/app_test.go +++ b/examples/basecoin/app/app_test.go @@ -10,7 +10,6 @@ import ( "github.com/stretchr/testify/require" "github.com/cosmos/cosmos-sdk/examples/basecoin/types" - "github.com/cosmos/cosmos-sdk/examples/basecoin/x/cool" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/x/auth" "github.com/cosmos/cosmos-sdk/x/bank" @@ -26,50 +25,93 @@ import ( var ( chainID = "" // TODO - priv1 = crypto.GenPrivKeyEd25519() - addr1 = priv1.PubKey().Address() - addr2 = crypto.GenPrivKeyEd25519().PubKey().Address() - coins = sdk.Coins{{"foocoin", 10}} - fee = sdk.StdFee{ + accName = "foobart" + + priv1 = crypto.GenPrivKeyEd25519() + addr1 = priv1.PubKey().Address() + priv2 = crypto.GenPrivKeyEd25519() + addr2 = priv2.PubKey().Address() + addr3 = crypto.GenPrivKeyEd25519().PubKey().Address() + priv4 = crypto.GenPrivKeyEd25519() + addr4 = priv4.PubKey().Address() + coins = sdk.Coins{{"foocoin", 10}} + halfCoins = sdk.Coins{{"foocoin", 5}} + fee = sdk.StdFee{ sdk.Coins{{"foocoin", 0}}, 0, } - sendMsg = bank.SendMsg{ + sendMsg1 = bank.SendMsg{ Inputs: []bank.Input{bank.NewInput(addr1, coins)}, Outputs: []bank.Output{bank.NewOutput(addr2, coins)}, } - quizMsg1 = cool.QuizMsg{ - Sender: addr1, - CoolAnswer: "icecold", + sendMsg2 = bank.SendMsg{ + Inputs: []bank.Input{bank.NewInput(addr1, coins)}, + Outputs: []bank.Output{ + bank.NewOutput(addr2, halfCoins), + bank.NewOutput(addr3, halfCoins), + }, } - quizMsg2 = cool.QuizMsg{ - Sender: addr1, - CoolAnswer: "badvibesonly", + sendMsg3 = bank.SendMsg{ + Inputs: []bank.Input{ + bank.NewInput(addr1, coins), + bank.NewInput(addr4, coins), + }, + Outputs: []bank.Output{ + bank.NewOutput(addr2, coins), + bank.NewOutput(addr3, coins), + }, } - setTrendMsg1 = cool.SetTrendMsg{ - Sender: addr1, - Cool: "icecold", - } - - setTrendMsg2 = cool.SetTrendMsg{ - Sender: addr1, - Cool: "badvibesonly", - } - - setTrendMsg3 = cool.SetTrendMsg{ - Sender: addr1, - Cool: "warmandkind", + sendMsg4 = bank.SendMsg{ + Inputs: []bank.Input{ + bank.NewInput(addr2, coins), + }, + Outputs: []bank.Output{ + bank.NewOutput(addr1, coins), + }, } ) -func newBasecoinApp() *BasecoinApp { +func loggerAndDBs() (log.Logger, map[string]dbm.DB) { logger := log.NewTMLogger(log.NewSyncWriter(os.Stdout)).With("module", "sdk/app") - db := dbm.NewMemDB() - return NewBasecoinApp(logger, db) + dbs := map[string]dbm.DB{ + "main": dbm.NewMemDB(), + "acc": dbm.NewMemDB(), + "ibc": dbm.NewMemDB(), + "staking": dbm.NewMemDB(), + } + return logger, dbs +} + +func newBasecoinApp() *BasecoinApp { + logger, dbs := loggerAndDBs() + return NewBasecoinApp(logger, dbs) +} + +func setGenesisAccounts(bapp *BasecoinApp, accs ...auth.BaseAccount) error { + genaccs := make([]*types.GenesisAccount, len(accs)) + for i, acc := range accs { + genaccs[i] = types.NewGenesisAccount(&types.AppAccount{acc, accName}) + } + + genesisState := types.GenesisState{ + Accounts: genaccs, + } + + stateBytes, err := json.MarshalIndent(genesisState, "", "\t") + if err != nil { + return err + } + + // Initialize the chain + vals := []abci.Validator{} + bapp.InitChain(abci.RequestInitChain{vals, stateBytes}) + bapp.Commit() + + return nil } //_______________________________________________________________________ @@ -80,41 +122,18 @@ func TestMsgs(t *testing.T) { msgs := []struct { msg sdk.Msg }{ - {sendMsg}, - {quizMsg1}, - {setTrendMsg1}, + {sendMsg1}, } - sequences := []int64{0} for i, m := range msgs { - sig := priv1.Sign(sdk.StdSignBytes(chainID, sequences, fee, m.msg)) - tx := sdk.NewStdTx(m.msg, fee, []sdk.StdSignature{{ - PubKey: priv1.PubKey(), - Signature: sig, - }}) - - // just marshal/unmarshal! - cdc := MakeCodec() - txBytes, err := cdc.MarshalBinary(tx) - require.NoError(t, err, "i: %v", i) - - // Run a Check - cres := bapp.CheckTx(txBytes) - assert.Equal(t, sdk.CodeUnknownAddress, - sdk.CodeType(cres.Code), "i: %v, log: %v", i, cres.Log) - - // Simulate a Block - bapp.BeginBlock(abci.RequestBeginBlock{}) - dres := bapp.DeliverTx(txBytes) - assert.Equal(t, sdk.CodeUnknownAddress, - sdk.CodeType(dres.Code), "i: %v, log: %v", i, dres.Log) + // Run a CheckDeliver + SignCheckDeliver(t, bapp, m.msg, []int64{int64(i)}, false, priv1) } } func TestGenesis(t *testing.T) { - logger := log.NewTMLogger(log.NewSyncWriter(os.Stdout)).With("module", "sdk/app") - db := dbm.NewMemDB() - bapp := NewBasecoinApp(logger, db) + logger, dbs := loggerAndDBs() + bapp := NewBasecoinApp(logger, dbs) // Construct some genesis bytes to reflect basecoin/types/AppAccount pk := crypto.GenPrivKeyEd25519().PubKey() @@ -127,28 +146,19 @@ func TestGenesis(t *testing.T) { } acc := &types.AppAccount{baseAcc, "foobart"} - genesisState := types.GenesisState{ - Accounts: []*types.GenesisAccount{ - types.NewGenesisAccount(acc), - }, - } - stateBytes, err := json.MarshalIndent(genesisState, "", "\t") - - vals := []abci.Validator{} - bapp.InitChain(abci.RequestInitChain{vals, stateBytes}) - bapp.Commit() + err = setGenesisAccounts(bapp, baseAcc) + assert.Nil(t, err) // A checkTx context ctx := bapp.BaseApp.NewContext(true, abci.Header{}) res1 := bapp.accountMapper.GetAccount(ctx, baseAcc.Address) assert.Equal(t, acc, res1) - /* - // reload app and ensure the account is still there - bapp = NewBasecoinApp(logger, db) - ctx = bapp.BaseApp.NewContext(true, abci.Header{}) - res1 = bapp.accountMapper.GetAccount(ctx, baseAcc.Address) - assert.Equal(t, acc, res1) - */ + + // reload app and ensure the account is still there + bapp = NewBasecoinApp(logger, dbs) + ctx = bapp.BaseApp.NewContext(true, abci.Header{}) + res1 = bapp.accountMapper.GetAccount(ctx, baseAcc.Address) + assert.Equal(t, acc, res1) } func TestSendMsgWithAccounts(t *testing.T) { @@ -162,66 +172,124 @@ func TestSendMsgWithAccounts(t *testing.T) { Address: addr1, Coins: coins, } - acc1 := &types.AppAccount{baseAcc, "foobart"} // Construct genesis state - genesisState := types.GenesisState{ - Accounts: []*types.GenesisAccount{ - types.NewGenesisAccount(acc1), - }, - } - stateBytes, err := json.MarshalIndent(genesisState, "", "\t") - require.Nil(t, err) - - // Initialize the chain - vals := []abci.Validator{} - bapp.InitChain(abci.RequestInitChain{vals, stateBytes}) - bapp.Commit() - + err = setGenesisAccounts(bapp, baseAcc) + assert.Nil(t, err) // A checkTx context (true) ctxCheck := bapp.BaseApp.NewContext(true, abci.Header{}) res1 := bapp.accountMapper.GetAccount(ctxCheck, addr1) - assert.Equal(t, acc1, res1) + assert.Equal(t, baseAcc, res1.(*types.AppAccount).BaseAccount) - // Sign the tx - sequences := []int64{0} - sig := priv1.Sign(sdk.StdSignBytes(chainID, sequences, fee, sendMsg)) - tx := sdk.NewStdTx(sendMsg, fee, []sdk.StdSignature{{ - PubKey: priv1.PubKey(), - Signature: sig, - }}) - - // Run a Check - res := bapp.Check(tx) - assert.Equal(t, sdk.CodeOK, res.Code, res.Log) - - // Simulate a Block - bapp.BeginBlock(abci.RequestBeginBlock{}) - res = bapp.Deliver(tx) - assert.Equal(t, sdk.CodeOK, res.Code, res.Log) + // Run a CheckDeliver + SignCheckDeliver(t, bapp, sendMsg1, []int64{0}, true, priv1) // Check balances - ctxDeliver := bapp.BaseApp.NewContext(false, abci.Header{}) - res2 := bapp.accountMapper.GetAccount(ctxDeliver, addr1) - res3 := bapp.accountMapper.GetAccount(ctxDeliver, addr2) - assert.Equal(t, fmt.Sprintf("%v", res2.GetCoins()), "67foocoin") - assert.Equal(t, fmt.Sprintf("%v", res3.GetCoins()), "10foocoin") + CheckBalance(t, bapp, addr1, "67foocoin") + CheckBalance(t, bapp, addr2, "10foocoin") // Delivering again should cause replay error - res = bapp.Deliver(tx) - assert.Equal(t, sdk.CodeInvalidSequence, res.Code, res.Log) + SignCheckDeliver(t, bapp, sendMsg1, []int64{0}, false, priv1) // bumping the txnonce number without resigning should be an auth error + tx := genTx(sendMsg1, []int64{0}, priv1) tx.Signatures[0].Sequence = 1 - res = bapp.Deliver(tx) + res := bapp.Deliver(tx) + assert.Equal(t, sdk.CodeUnauthorized, res.Code, res.Log) // resigning the tx with the bumped sequence should work - sequences = []int64{1} - sig = priv1.Sign(sdk.StdSignBytes(chainID, sequences, fee, tx.Msg)) - tx.Signatures[0].Signature = sig - res = bapp.Deliver(tx) - assert.Equal(t, sdk.CodeOK, res.Code, res.Log) + SignCheckDeliver(t, bapp, sendMsg1, []int64{1}, true, priv1) +} + +func TestSendMsgMultipleOut(t *testing.T) { + bapp := newBasecoinApp() + + genCoins, err := sdk.ParseCoins("42foocoin") + require.Nil(t, err) + + acc1 := auth.BaseAccount{ + Address: addr1, + Coins: genCoins, + } + + acc2 := auth.BaseAccount{ + Address: addr2, + Coins: genCoins, + } + + err = setGenesisAccounts(bapp, acc1, acc2) + assert.Nil(t, err) + + // Simulate a Block + SignCheckDeliver(t, bapp, sendMsg2, []int64{0}, true, priv1) + + // Check balances + CheckBalance(t, bapp, addr1, "32foocoin") + CheckBalance(t, bapp, addr2, "47foocoin") + CheckBalance(t, bapp, addr3, "5foocoin") +} + +func TestSengMsgMultipleInOut(t *testing.T) { + bapp := newBasecoinApp() + + genCoins, err := sdk.ParseCoins("42foocoin") + require.Nil(t, err) + + acc1 := auth.BaseAccount{ + Address: addr1, + Coins: genCoins, + } + + acc2 := auth.BaseAccount{ + Address: addr2, + Coins: genCoins, + } + + acc4 := auth.BaseAccount{ + Address: addr4, + Coins: genCoins, + } + + err = setGenesisAccounts(bapp, acc1, acc2, acc4) + assert.Nil(t, err) + + // CheckDeliver + SignCheckDeliver(t, bapp, sendMsg3, []int64{0, 0}, true, priv1, priv4) + + // Check balances + CheckBalance(t, bapp, addr1, "32foocoin") + CheckBalance(t, bapp, addr4, "32foocoin") + CheckBalance(t, bapp, addr2, "52foocoin") + CheckBalance(t, bapp, addr3, "10foocoin") +} + +func TestSendMsgDependent(t *testing.T) { + bapp := newBasecoinApp() + + genCoins, err := sdk.ParseCoins("42foocoin") + require.Nil(t, err) + + acc1 := auth.BaseAccount{ + Address: addr1, + Coins: genCoins, + } + + err = setGenesisAccounts(bapp, acc1) + assert.Nil(t, err) + + // CheckDeliver + SignCheckDeliver(t, bapp, sendMsg1, []int64{0}, true, priv1) + + // Check balances + CheckBalance(t, bapp, addr1, "32foocoin") + CheckBalance(t, bapp, addr2, "10foocoin") + + // Simulate a Block + SignCheckDeliver(t, bapp, sendMsg4, []int64{0}, true, priv2) + + // Check balances + CheckBalance(t, bapp, addr1, "42foocoin") } func TestQuizMsg(t *testing.T) { @@ -237,8 +305,8 @@ func TestQuizMsg(t *testing.T) { acc1 := &types.AppAccount{baseAcc, "foobart"} // Construct genesis state - genesisState := types.GenesisState{ - Accounts: []*types.GenesisAccount{ + genesisState := map[string]interface{}{ + "accounts": []*types.GenesisAccount{ types.NewGenesisAccount(acc1), }, } @@ -255,45 +323,22 @@ func TestQuizMsg(t *testing.T) { res1 := bapp.accountMapper.GetAccount(ctxCheck, addr1) assert.Equal(t, acc1, res1) - // Set the trend, submit a really cool quiz and check for reward - SignCheckDeliver(t, bapp, setTrendMsg1, 0, true) - SignCheckDeliver(t, bapp, quizMsg1, 1, true) - CheckBalance(t, bapp, "69icecold") - SignCheckDeliver(t, bapp, quizMsg2, 2, true) // result without reward - CheckBalance(t, bapp, "69icecold") - SignCheckDeliver(t, bapp, quizMsg1, 3, true) - CheckBalance(t, bapp, "138icecold") - SignCheckDeliver(t, bapp, setTrendMsg2, 4, true) // reset the trend - SignCheckDeliver(t, bapp, quizMsg1, 5, true) // the same answer will nolonger do! - CheckBalance(t, bapp, "138icecold") - SignCheckDeliver(t, bapp, quizMsg2, 6, true) // earlier answer now relavent again - CheckBalance(t, bapp, "69badvibesonly,138icecold") - SignCheckDeliver(t, bapp, setTrendMsg3, 7, false) // expect to fail to set the trend to something which is not cool - } -func TestHandler(t *testing.T) { +func TestIBCMsgs(t *testing.T) { bapp := newBasecoinApp() sourceChain := "source-chain" destChain := "dest-chain" - vals := []abci.Validator{} baseAcc := auth.BaseAccount{ Address: addr1, Coins: coins, } acc1 := &types.AppAccount{baseAcc, "foobart"} - genesisState := types.GenesisState{ - Accounts: []*types.GenesisAccount{ - types.NewGenesisAccount(acc1), - }, - } - stateBytes, err := json.MarshalIndent(genesisState, "", "\t") - require.Nil(t, err) - bapp.InitChain(abci.RequestInitChain{vals, stateBytes}) - bapp.Commit() + err := setGenesisAccounts(bapp, baseAcc) + assert.Nil(t, err) // A checkTx context (true) ctxCheck := bapp.BaseApp.NewContext(true, abci.Header{}) res1 := bapp.accountMapper.GetAccount(ctxCheck, addr1) @@ -317,23 +362,32 @@ func TestHandler(t *testing.T) { Sequence: 0, } - SignCheckDeliver(t, bapp, transferMsg, 0, true) - CheckBalance(t, bapp, "") - SignCheckDeliver(t, bapp, transferMsg, 1, false) - SignCheckDeliver(t, bapp, receiveMsg, 2, true) - CheckBalance(t, bapp, "10foocoin") - SignCheckDeliver(t, bapp, receiveMsg, 3, false) + SignCheckDeliver(t, bapp, transferMsg, []int64{0}, true, priv1) + CheckBalance(t, bapp, addr1, "") + SignCheckDeliver(t, bapp, transferMsg, []int64{1}, false, priv1) + SignCheckDeliver(t, bapp, receiveMsg, []int64{2}, true, priv1) + CheckBalance(t, bapp, addr1, "10foocoin") + SignCheckDeliver(t, bapp, receiveMsg, []int64{3}, false, priv1) } -func SignCheckDeliver(t *testing.T, bapp *BasecoinApp, msg sdk.Msg, seq int64, expPass bool) { +func genTx(msg sdk.Msg, seq []int64, priv ...crypto.PrivKeyEd25519) sdk.StdTx { + sigs := make([]sdk.StdSignature, len(priv)) + for i, p := range priv { + sigs[i] = sdk.StdSignature{ + PubKey: p.PubKey(), + Signature: p.Sign(sdk.StdSignBytes(chainID, seq, fee, msg)), + Sequence: seq[i], + } + } + + return sdk.NewStdTx(msg, fee, sigs) + +} + +func SignCheckDeliver(t *testing.T, bapp *BasecoinApp, msg sdk.Msg, seq []int64, expPass bool, priv ...crypto.PrivKeyEd25519) { // Sign the tx - tx := sdk.NewStdTx(msg, fee, []sdk.StdSignature{{ - PubKey: priv1.PubKey(), - Signature: priv1.Sign(sdk.StdSignBytes(chainID, []int64{seq}, fee, msg)), - Sequence: seq, - }}) - + tx := genTx(msg, seq, priv...) // Run a Check res := bapp.Check(tx) if expPass { @@ -350,10 +404,12 @@ func SignCheckDeliver(t *testing.T, bapp *BasecoinApp, msg sdk.Msg, seq int64, e } else { require.NotEqual(t, sdk.CodeOK, res.Code, res.Log) } + bapp.EndBlock(abci.RequestEndBlock{}) + //bapp.Commit() } -func CheckBalance(t *testing.T, bapp *BasecoinApp, balExpected string) { +func CheckBalance(t *testing.T, bapp *BasecoinApp, addr sdk.Address, balExpected string) { ctxDeliver := bapp.BaseApp.NewContext(false, abci.Header{}) - res2 := bapp.accountMapper.GetAccount(ctxDeliver, addr1) + res2 := bapp.accountMapper.GetAccount(ctxDeliver, addr) assert.Equal(t, balExpected, fmt.Sprintf("%v", res2.GetCoins())) } diff --git a/examples/basecoin/cmd/basecli/main.go b/examples/basecoin/cmd/basecli/main.go index 6fabc612c..a0152aee9 100644 --- a/examples/basecoin/cmd/basecli/main.go +++ b/examples/basecoin/cmd/basecli/main.go @@ -2,9 +2,10 @@ package main import ( "errors" - "github.com/spf13/cobra" "os" + "github.com/spf13/cobra" + "github.com/tendermint/tmlibs/cli" "github.com/cosmos/cosmos-sdk/client" @@ -13,12 +14,11 @@ import ( "github.com/cosmos/cosmos-sdk/client/rpc" "github.com/cosmos/cosmos-sdk/client/tx" - coolcmd "github.com/cosmos/cosmos-sdk/examples/basecoin/x/cool/commands" "github.com/cosmos/cosmos-sdk/version" authcmd "github.com/cosmos/cosmos-sdk/x/auth/commands" bankcmd "github.com/cosmos/cosmos-sdk/x/bank/commands" ibccmd "github.com/cosmos/cosmos-sdk/x/ibc/commands" - stakingcmd "github.com/cosmos/cosmos-sdk/x/staking/commands" + simplestakingcmd "github.com/cosmos/cosmos-sdk/x/simplestake/commands" "github.com/cosmos/cosmos-sdk/examples/basecoin/app" "github.com/cosmos/cosmos-sdk/examples/basecoin/types" @@ -62,14 +62,6 @@ func main() { client.PostCommands( bankcmd.SendTxCmd(cdc), )...) - basecliCmd.AddCommand( - client.PostCommands( - coolcmd.QuizTxCmd(cdc), - )...) - basecliCmd.AddCommand( - client.PostCommands( - coolcmd.SetTrendTxCmd(cdc), - )...) basecliCmd.AddCommand( client.PostCommands( ibccmd.IBCTransferCmd(cdc), @@ -77,11 +69,11 @@ func main() { basecliCmd.AddCommand( client.PostCommands( ibccmd.IBCRelayCmd(cdc), - stakingcmd.BondTxCmd(cdc), + simplestakingcmd.BondTxCmd(cdc), )...) basecliCmd.AddCommand( client.PostCommands( - stakingcmd.UnbondTxCmd(cdc), + simplestakingcmd.UnbondTxCmd(cdc), )...) // add proxy, version and key info diff --git a/examples/basecoin/cmd/basecoind/main.go b/examples/basecoin/cmd/basecoind/main.go index ea49c0b97..395667671 100644 --- a/examples/basecoin/cmd/basecoind/main.go +++ b/examples/basecoin/cmd/basecoind/main.go @@ -4,11 +4,13 @@ import ( "encoding/json" "fmt" "os" + "path/filepath" "github.com/spf13/cobra" abci "github.com/tendermint/abci/types" "github.com/tendermint/tmlibs/cli" + cmn "github.com/tendermint/tmlibs/common" dbm "github.com/tendermint/tmlibs/db" "github.com/tendermint/tmlibs/log" @@ -27,14 +29,11 @@ var ( // defaultOptions sets up the app_options for the // default genesis file -func defaultOptions(args []string) (json.RawMessage, error) { +func defaultOptions(args []string) (json.RawMessage, string, cmn.HexBytes, error) { addr, secret, err := server.GenerateCoinKey() if err != nil { - return nil, err + return nil, "", nil, err } - fmt.Println("Secret phrase to access coins:") - fmt.Println(secret) - opts := fmt.Sprintf(`{ "accounts": [{ "address": "%s", @@ -46,15 +45,33 @@ func defaultOptions(args []string) (json.RawMessage, error) { ] }] }`, addr) - return json.RawMessage(opts), nil + return json.RawMessage(opts), secret, addr, nil } func generateApp(rootDir string, logger log.Logger) (abci.Application, error) { - db, err := dbm.NewGoLevelDB("basecoin", rootDir) + dbMain, err := dbm.NewGoLevelDB("basecoin", filepath.Join(rootDir, "data")) if err != nil { return nil, err } - bapp := app.NewBasecoinApp(logger, db) + dbAcc, err := dbm.NewGoLevelDB("basecoin-acc", filepath.Join(rootDir, "data")) + if err != nil { + return nil, err + } + dbIBC, err := dbm.NewGoLevelDB("basecoin-ibc", filepath.Join(rootDir, "data")) + if err != nil { + return nil, err + } + dbStaking, err := dbm.NewGoLevelDB("basecoin-staking", filepath.Join(rootDir, "data")) + if err != nil { + return nil, err + } + dbs := map[string]dbm.DB{ + "main": dbMain, + "acc": dbAcc, + "ibc": dbIBC, + "staking": dbStaking, + } + bapp := app.NewBasecoinApp(logger, dbs) return bapp, nil } @@ -68,6 +85,7 @@ func main() { server.StartCmd(generateApp, logger), server.UnsafeResetAllCmd(logger), server.ShowNodeIdCmd(logger), + server.ShowValidatorCmd(logger), version.VersionCmd, ) diff --git a/examples/basecoin/x/cool/mapper.go b/examples/basecoin/x/cool/mapper.go deleted file mode 100644 index 2e0a791fa..000000000 --- a/examples/basecoin/x/cool/mapper.go +++ /dev/null @@ -1,30 +0,0 @@ -package cool - -import ( - sdk "github.com/cosmos/cosmos-sdk/types" -) - -// This Cool Mapper handlers sets/gets of custom variables for your module -type Mapper struct { - key sdk.StoreKey // The (unexposed) key used to access the store from the Context. -} - -func NewMapper(key sdk.StoreKey) Mapper { - return Mapper{key} -} - -// Key to knowing the trend on the streets! -var trendKey = []byte("TrendKey") - -// Implements sdk.AccountMapper. -func (am Mapper) GetTrend(ctx sdk.Context) string { - store := ctx.KVStore(am.key) - bz := store.Get(trendKey) - return string(bz) -} - -// Implements sdk.AccountMapper. -func (am Mapper) SetTrend(ctx sdk.Context, newTrend string) { - store := ctx.KVStore(am.key) - store.Set(trendKey, []byte(newTrend)) -} diff --git a/examples/democoin/LICENSE b/examples/democoin/LICENSE new file mode 100644 index 000000000..1697a7443 --- /dev/null +++ b/examples/democoin/LICENSE @@ -0,0 +1,204 @@ +Cosmos-SDK Democoin (template) +License: Apache2.0 + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2018 All in Bits, Inc + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/examples/democoin/Makefile b/examples/democoin/Makefile new file mode 100644 index 000000000..067d03e9b --- /dev/null +++ b/examples/democoin/Makefile @@ -0,0 +1,22 @@ +PACKAGES=$(shell go list ./... | grep -v '/vendor/') +BUILD_FLAGS = -ldflags "-X github.com/cosmos/cosmos-sdk/examples/democoin/version.GitCommit=`git rev-parse --short HEAD`" + +all: get_tools get_vendor_deps build test + +get_tools: + go get github.com/golang/dep/cmd/dep + +build: + go build $(BUILD_FLAGS) -o build/democoin ./cmd/... + +get_vendor_deps: + @rm -rf vendor/ + @dep ensure + +test: + @go test $(PACKAGES) + +benchmark: + @go test -bench=. $(PACKAGES) + +.PHONY: all build test benchmark diff --git a/examples/democoin/README.md b/examples/democoin/README.md new file mode 100644 index 000000000..fe65abda4 --- /dev/null +++ b/examples/democoin/README.md @@ -0,0 +1,70 @@ +# Democoin + +This is the "Democoin" example application built on the Cosmos-Sdk. This +"Democoin" is not affiliated with [Coinbase](http://www.getdemocoin.com/), nor +the [stable coin](http://www.getdemocoin.com/). + +Assuming you've run `make get_tools && make get_vendor_deps` from the root of +this repository, run `make build` here to build the `democoind` and `basecli` +binaries. + +If you want to create a new application, start by copying the Democoin app. + + +# Building your own Blockchain + +Democoin is the equivalent of an ERC20 token contract for blockchains. In order +to deploy your own application all you need to do is clone `examples/democoin` +and run it. Now you are already running your own blockchain. In the following +I will explain how to add functionality to your blockchain. This is akin to +defining your own vesting schedule within a contract or setting a specific +multisig. You are just extending the base layer with extra functionality here +and there. + +## Structure of Democoin + +Democoin is build with the cosmos-sdk. It is a sample application that works +with any engine that implements the ABCI protocol. Democoin defines multiple +unique modules as well as uses modules directly from the sdk. If you want +to modify Democoin, you either remove or add modules according to your wishes. + + +## Modules + +A module is a fundamental unit in the cosmos-sdk. A module defines its own +transaction, handles its own state as well as its own state transition logic. +Globally, in the `app/app.go` file you just have to define a key for that +module to access some parts of the state, as well as initialise the module +object and finally add it to the transaction router. The router ensures that +every module only gets its own messages. + + +## Transactions + +A user can send a transaction to the running blockchain application. This +transaction can be of any of the ones that are supported by any of the +registered modules. + +### CheckTx + +Once a user has submitted their transaction to the engine, +the engine will first run `checkTx` to confirm that it is a valid transaction. +The module has to define a handler that knows how to handle every transaction +type. The corresponding handler gets invoked with the checkTx flag set to true. +This means that the handler shouldn't do any expensive operations, but it can +and should write to the checkTx state. + +### DeliverTx + +The engine calls `deliverTx` when a new block has been agreed upon in +consensus. Again, the corresponding module will have its handler invoked +and the state and context is passed in. During deliverTx execution the +transaction needs to be processed fully and the results are written to the +application state. + + +## CLI + +The cosmos-sdk contains a number of helper libraries in `clients/` to build cli +and RPC interfaces for your specific application. + diff --git a/examples/democoin/app/app.go b/examples/democoin/app/app.go new file mode 100644 index 000000000..2fffb8324 --- /dev/null +++ b/examples/democoin/app/app.go @@ -0,0 +1,175 @@ +package app + +import ( + "encoding/json" + + abci "github.com/tendermint/abci/types" + oldwire "github.com/tendermint/go-wire" + cmn "github.com/tendermint/tmlibs/common" + dbm "github.com/tendermint/tmlibs/db" + "github.com/tendermint/tmlibs/log" + + bam "github.com/cosmos/cosmos-sdk/baseapp" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/wire" + "github.com/cosmos/cosmos-sdk/x/auth" + "github.com/cosmos/cosmos-sdk/x/bank" + "github.com/cosmos/cosmos-sdk/x/ibc" + "github.com/cosmos/cosmos-sdk/x/simplestake" + + "github.com/cosmos/cosmos-sdk/examples/democoin/types" + "github.com/cosmos/cosmos-sdk/examples/democoin/x/cool" + "github.com/cosmos/cosmos-sdk/examples/democoin/x/sketchy" +) + +const ( + appName = "DemocoinApp" +) + +// Extended ABCI application +type DemocoinApp struct { + *bam.BaseApp + cdc *wire.Codec + + // keys to access the substores + capKeyMainStore *sdk.KVStoreKey + capKeyAccountStore *sdk.KVStoreKey + capKeyIBCStore *sdk.KVStoreKey + capKeyStakingStore *sdk.KVStoreKey + + // Manage getting and setting accounts + accountMapper sdk.AccountMapper +} + +func NewDemocoinApp(logger log.Logger, dbs map[string]dbm.DB) *DemocoinApp { + // create your application object + var app = &DemocoinApp{ + BaseApp: bam.NewBaseApp(appName, logger, dbs["main"]), + cdc: MakeCodec(), + capKeyMainStore: sdk.NewKVStoreKey("main"), + capKeyAccountStore: sdk.NewKVStoreKey("acc"), + capKeyIBCStore: sdk.NewKVStoreKey("ibc"), + capKeyStakingStore: sdk.NewKVStoreKey("stake"), + } + + // define the accountMapper + app.accountMapper = auth.NewAccountMapperSealed( + app.capKeyMainStore, // target store + &types.AppAccount{}, // prototype + ) + + // add handlers + coinKeeper := bank.NewCoinKeeper(app.accountMapper) + coolKeeper := cool.NewKeeper(app.capKeyMainStore, coinKeeper) + ibcMapper := ibc.NewIBCMapper(app.cdc, app.capKeyIBCStore) + stakeKeeper := simplestake.NewKeeper(app.capKeyStakingStore, coinKeeper) + app.Router(). + AddRoute("bank", bank.NewHandler(coinKeeper)). + AddRoute("cool", cool.NewHandler(coolKeeper)). + AddRoute("sketchy", sketchy.NewHandler()). + AddRoute("ibc", ibc.NewHandler(ibcMapper, coinKeeper)). + AddRoute("simplestake", simplestake.NewHandler(stakeKeeper)) + + // initialize BaseApp + app.SetTxDecoder(app.txDecoder) + app.SetInitChainer(app.initChainerFn(coolKeeper)) + app.MountStoreWithDB(app.capKeyMainStore, sdk.StoreTypeIAVL, dbs["main"]) + app.MountStoreWithDB(app.capKeyAccountStore, sdk.StoreTypeIAVL, dbs["acc"]) + app.MountStoreWithDB(app.capKeyIBCStore, sdk.StoreTypeIAVL, dbs["ibc"]) + app.MountStoreWithDB(app.capKeyStakingStore, sdk.StoreTypeIAVL, dbs["staking"]) + // NOTE: Broken until #532 lands + //app.MountStoresIAVL(app.capKeyMainStore, app.capKeyIBCStore, app.capKeyStakingStore) + app.SetAnteHandler(auth.NewAnteHandler(app.accountMapper)) + err := app.LoadLatestVersion(app.capKeyMainStore) + if err != nil { + cmn.Exit(err.Error()) + } + + return app +} + +// custom tx codec +// TODO: use new go-wire +func MakeCodec() *wire.Codec { + const msgTypeSend = 0x1 + const msgTypeIssue = 0x2 + const msgTypeQuiz = 0x3 + const msgTypeSetTrend = 0x4 + const msgTypeIBCTransferMsg = 0x5 + const msgTypeIBCReceiveMsg = 0x6 + const msgTypeBondMsg = 0x7 + const msgTypeUnbondMsg = 0x8 + var _ = oldwire.RegisterInterface( + struct{ sdk.Msg }{}, + oldwire.ConcreteType{bank.SendMsg{}, msgTypeSend}, + oldwire.ConcreteType{bank.IssueMsg{}, msgTypeIssue}, + oldwire.ConcreteType{cool.QuizMsg{}, msgTypeQuiz}, + oldwire.ConcreteType{cool.SetTrendMsg{}, msgTypeSetTrend}, + oldwire.ConcreteType{ibc.IBCTransferMsg{}, msgTypeIBCTransferMsg}, + oldwire.ConcreteType{ibc.IBCReceiveMsg{}, msgTypeIBCReceiveMsg}, + oldwire.ConcreteType{simplestake.BondMsg{}, msgTypeBondMsg}, + oldwire.ConcreteType{simplestake.UnbondMsg{}, msgTypeUnbondMsg}, + ) + + const accTypeApp = 0x1 + var _ = oldwire.RegisterInterface( + struct{ sdk.Account }{}, + oldwire.ConcreteType{&types.AppAccount{}, accTypeApp}, + ) + cdc := wire.NewCodec() + + // cdc.RegisterInterface((*sdk.Msg)(nil), nil) + // bank.RegisterWire(cdc) // Register bank.[SendMsg,IssueMsg] types. + // crypto.RegisterWire(cdc) // Register crypto.[PubKey,PrivKey,Signature] types. + // ibc.RegisterWire(cdc) // Register ibc.[IBCTransferMsg, IBCReceiveMsg] types. + return cdc +} + +// custom logic for transaction decoding +func (app *DemocoinApp) txDecoder(txBytes []byte) (sdk.Tx, sdk.Error) { + var tx = sdk.StdTx{} + + if len(txBytes) == 0 { + return nil, sdk.ErrTxDecode("txBytes are empty") + } + + // StdTx.Msg is an interface. The concrete types + // are registered by MakeTxCodec in bank.RegisterWire. + err := app.cdc.UnmarshalBinary(txBytes, &tx) + if err != nil { + return nil, sdk.ErrTxDecode("").TraceCause(err, "") + } + return tx, nil +} + +// custom logic for democoin initialization +func (app *DemocoinApp) initChainerFn(coolKeeper cool.Keeper) sdk.InitChainer { + return func(ctx sdk.Context, req abci.RequestInitChain) abci.ResponseInitChain { + stateJSON := req.AppStateBytes + + genesisState := new(types.GenesisState) + err := json.Unmarshal(stateJSON, genesisState) + if err != nil { + panic(err) // TODO https://github.com/cosmos/cosmos-sdk/issues/468 + // return sdk.ErrGenesisParse("").TraceCause(err, "") + } + + for _, gacc := range genesisState.Accounts { + acc, err := gacc.ToAppAccount() + if err != nil { + panic(err) // TODO https://github.com/cosmos/cosmos-sdk/issues/468 + // return sdk.ErrGenesisParse("").TraceCause(err, "") + } + app.accountMapper.SetAccount(ctx, acc) + } + + // Application specific genesis handling + err = coolKeeper.InitGenesis(ctx, stateJSON) + if err != nil { + panic(err) // TODO https://github.com/cosmos/cosmos-sdk/issues/468 + // return sdk.ErrGenesisParse("").TraceCause(err, "") + } + + return abci.ResponseInitChain{} + } +} diff --git a/examples/democoin/app/app_test.go b/examples/democoin/app/app_test.go new file mode 100644 index 000000000..bf2ddc232 --- /dev/null +++ b/examples/democoin/app/app_test.go @@ -0,0 +1,382 @@ +package app + +import ( + "encoding/json" + "fmt" + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/cosmos/cosmos-sdk/examples/democoin/types" + "github.com/cosmos/cosmos-sdk/examples/democoin/x/cool" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/auth" + "github.com/cosmos/cosmos-sdk/x/bank" + "github.com/cosmos/cosmos-sdk/x/ibc" + + abci "github.com/tendermint/abci/types" + crypto "github.com/tendermint/go-crypto" + dbm "github.com/tendermint/tmlibs/db" + "github.com/tendermint/tmlibs/log" +) + +// Construct some global addrs and txs for tests. +var ( + chainID = "" // TODO + + priv1 = crypto.GenPrivKeyEd25519() + addr1 = priv1.PubKey().Address() + addr2 = crypto.GenPrivKeyEd25519().PubKey().Address() + coins = sdk.Coins{{"foocoin", 10}} + fee = sdk.StdFee{ + sdk.Coins{{"foocoin", 0}}, + 0, + } + + sendMsg = bank.SendMsg{ + Inputs: []bank.Input{bank.NewInput(addr1, coins)}, + Outputs: []bank.Output{bank.NewOutput(addr2, coins)}, + } + + quizMsg1 = cool.QuizMsg{ + Sender: addr1, + CoolAnswer: "icecold", + } + + quizMsg2 = cool.QuizMsg{ + Sender: addr1, + CoolAnswer: "badvibesonly", + } + + setTrendMsg1 = cool.SetTrendMsg{ + Sender: addr1, + Cool: "icecold", + } + + setTrendMsg2 = cool.SetTrendMsg{ + Sender: addr1, + Cool: "badvibesonly", + } + + setTrendMsg3 = cool.SetTrendMsg{ + Sender: addr1, + Cool: "warmandkind", + } +) + +func loggerAndDBs() (log.Logger, map[string]dbm.DB) { + logger := log.NewTMLogger(log.NewSyncWriter(os.Stdout)).With("module", "sdk/app") + dbs := map[string]dbm.DB{ + "main": dbm.NewMemDB(), + "acc": dbm.NewMemDB(), + "ibc": dbm.NewMemDB(), + "staking": dbm.NewMemDB(), + } + return logger, dbs +} + +func newDemocoinApp() *DemocoinApp { + logger, dbs := loggerAndDBs() + return NewDemocoinApp(logger, dbs) +} + +//_______________________________________________________________________ + +func TestMsgs(t *testing.T) { + bapp := newDemocoinApp() + + msgs := []struct { + msg sdk.Msg + }{ + {sendMsg}, + {quizMsg1}, + {setTrendMsg1}, + } + + sequences := []int64{0} + for i, m := range msgs { + sig := priv1.Sign(sdk.StdSignBytes(chainID, sequences, fee, m.msg)) + tx := sdk.NewStdTx(m.msg, fee, []sdk.StdSignature{{ + PubKey: priv1.PubKey(), + Signature: sig, + }}) + + // just marshal/unmarshal! + cdc := MakeCodec() + txBytes, err := cdc.MarshalBinary(tx) + require.NoError(t, err, "i: %v", i) + + // Run a Check + cres := bapp.CheckTx(txBytes) + assert.Equal(t, sdk.CodeUnknownAddress, + sdk.CodeType(cres.Code), "i: %v, log: %v", i, cres.Log) + + // Simulate a Block + bapp.BeginBlock(abci.RequestBeginBlock{}) + dres := bapp.DeliverTx(txBytes) + assert.Equal(t, sdk.CodeUnknownAddress, + sdk.CodeType(dres.Code), "i: %v, log: %v", i, dres.Log) + } +} + +func TestGenesis(t *testing.T) { + logger, dbs := loggerAndDBs() + bapp := NewDemocoinApp(logger, dbs) + + // Construct some genesis bytes to reflect democoin/types/AppAccount + pk := crypto.GenPrivKeyEd25519().PubKey() + addr := pk.Address() + coins, err := sdk.ParseCoins("77foocoin,99barcoin") + require.Nil(t, err) + baseAcc := auth.BaseAccount{ + Address: addr, + Coins: coins, + } + acc := &types.AppAccount{baseAcc, "foobart"} + + genesisState := map[string]interface{}{ + "accounts": []*types.GenesisAccount{ + types.NewGenesisAccount(acc), + }, + "cool": map[string]string{ + "trend": "ice-cold", + }, + } + stateBytes, err := json.MarshalIndent(genesisState, "", "\t") + + vals := []abci.Validator{} + bapp.InitChain(abci.RequestInitChain{vals, stateBytes}) + bapp.Commit() + + // A checkTx context + ctx := bapp.BaseApp.NewContext(true, abci.Header{}) + res1 := bapp.accountMapper.GetAccount(ctx, baseAcc.Address) + assert.Equal(t, acc, res1) + + // reload app and ensure the account is still there + bapp = NewDemocoinApp(logger, dbs) + ctx = bapp.BaseApp.NewContext(true, abci.Header{}) + res1 = bapp.accountMapper.GetAccount(ctx, baseAcc.Address) + assert.Equal(t, acc, res1) +} + +func TestSendMsgWithAccounts(t *testing.T) { + bapp := newDemocoinApp() + + // Construct some genesis bytes to reflect democoin/types/AppAccount + // Give 77 foocoin to the first key + coins, err := sdk.ParseCoins("77foocoin") + require.Nil(t, err) + baseAcc := auth.BaseAccount{ + Address: addr1, + Coins: coins, + } + acc1 := &types.AppAccount{baseAcc, "foobart"} + + // Construct genesis state + genesisState := map[string]interface{}{ + "accounts": []*types.GenesisAccount{ + types.NewGenesisAccount(acc1), + }, + "cool": map[string]string{ + "trend": "ice-cold", + }, + } + stateBytes, err := json.MarshalIndent(genesisState, "", "\t") + require.Nil(t, err) + + // Initialize the chain + vals := []abci.Validator{} + bapp.InitChain(abci.RequestInitChain{vals, stateBytes}) + bapp.Commit() + + // A checkTx context (true) + ctxCheck := bapp.BaseApp.NewContext(true, abci.Header{}) + res1 := bapp.accountMapper.GetAccount(ctxCheck, addr1) + assert.Equal(t, acc1, res1) + + // Sign the tx + sequences := []int64{0} + sig := priv1.Sign(sdk.StdSignBytes(chainID, sequences, fee, sendMsg)) + tx := sdk.NewStdTx(sendMsg, fee, []sdk.StdSignature{{ + PubKey: priv1.PubKey(), + Signature: sig, + }}) + + // Run a Check + res := bapp.Check(tx) + assert.Equal(t, sdk.CodeOK, res.Code, res.Log) + + // Simulate a Block + bapp.BeginBlock(abci.RequestBeginBlock{}) + res = bapp.Deliver(tx) + assert.Equal(t, sdk.CodeOK, res.Code, res.Log) + + // Check balances + ctxDeliver := bapp.BaseApp.NewContext(false, abci.Header{}) + res2 := bapp.accountMapper.GetAccount(ctxDeliver, addr1) + res3 := bapp.accountMapper.GetAccount(ctxDeliver, addr2) + assert.Equal(t, fmt.Sprintf("%v", res2.GetCoins()), "67foocoin") + assert.Equal(t, fmt.Sprintf("%v", res3.GetCoins()), "10foocoin") + + // Delivering again should cause replay error + res = bapp.Deliver(tx) + assert.Equal(t, sdk.CodeInvalidSequence, res.Code, res.Log) + + // bumping the txnonce number without resigning should be an auth error + tx.Signatures[0].Sequence = 1 + res = bapp.Deliver(tx) + assert.Equal(t, sdk.CodeUnauthorized, res.Code, res.Log) + + // resigning the tx with the bumped sequence should work + sequences = []int64{1} + sig = priv1.Sign(sdk.StdSignBytes(chainID, sequences, fee, tx.Msg)) + tx.Signatures[0].Signature = sig + res = bapp.Deliver(tx) + assert.Equal(t, sdk.CodeOK, res.Code, res.Log) +} + +func TestQuizMsg(t *testing.T) { + bapp := newDemocoinApp() + + // Construct genesis state + // Construct some genesis bytes to reflect democoin/types/AppAccount + coins := sdk.Coins{} + baseAcc := auth.BaseAccount{ + Address: addr1, + Coins: coins, + } + acc1 := &types.AppAccount{baseAcc, "foobart"} + + // Construct genesis state + genesisState := map[string]interface{}{ + "accounts": []*types.GenesisAccount{ + types.NewGenesisAccount(acc1), + }, + "cool": map[string]string{ + "trend": "ice-cold", + }, + } + stateBytes, err := json.MarshalIndent(genesisState, "", "\t") + require.Nil(t, err) + + // Initialize the chain (nil) + vals := []abci.Validator{} + bapp.InitChain(abci.RequestInitChain{vals, stateBytes}) + bapp.Commit() + + // A checkTx context (true) + ctxCheck := bapp.BaseApp.NewContext(true, abci.Header{}) + res1 := bapp.accountMapper.GetAccount(ctxCheck, addr1) + assert.Equal(t, acc1, res1) + + // Set the trend, submit a really cool quiz and check for reward + SignCheckDeliver(t, bapp, setTrendMsg1, 0, true) + SignCheckDeliver(t, bapp, quizMsg1, 1, true) + CheckBalance(t, bapp, "69icecold") + SignCheckDeliver(t, bapp, quizMsg2, 2, false) // result without reward + CheckBalance(t, bapp, "69icecold") + SignCheckDeliver(t, bapp, quizMsg1, 3, true) + CheckBalance(t, bapp, "138icecold") + SignCheckDeliver(t, bapp, setTrendMsg2, 4, true) // reset the trend + SignCheckDeliver(t, bapp, quizMsg1, 5, false) // the same answer will nolonger do! + CheckBalance(t, bapp, "138icecold") + SignCheckDeliver(t, bapp, quizMsg2, 6, true) // earlier answer now relavent again + CheckBalance(t, bapp, "69badvibesonly,138icecold") + SignCheckDeliver(t, bapp, setTrendMsg3, 7, false) // expect to fail to set the trend to something which is not cool + +} + +func TestHandler(t *testing.T) { + bapp := newDemocoinApp() + + sourceChain := "source-chain" + destChain := "dest-chain" + + vals := []abci.Validator{} + baseAcc := auth.BaseAccount{ + Address: addr1, + Coins: coins, + } + acc1 := &types.AppAccount{baseAcc, "foobart"} + genesisState := map[string]interface{}{ + "accounts": []*types.GenesisAccount{ + types.NewGenesisAccount(acc1), + }, + "cool": map[string]string{ + "trend": "ice-cold", + }, + } + stateBytes, err := json.MarshalIndent(genesisState, "", "\t") + require.Nil(t, err) + bapp.InitChain(abci.RequestInitChain{vals, stateBytes}) + bapp.Commit() + + // A checkTx context (true) + ctxCheck := bapp.BaseApp.NewContext(true, abci.Header{}) + res1 := bapp.accountMapper.GetAccount(ctxCheck, addr1) + assert.Equal(t, acc1, res1) + + packet := ibc.IBCPacket{ + SrcAddr: addr1, + DestAddr: addr1, + Coins: coins, + SrcChain: sourceChain, + DestChain: destChain, + } + + transferMsg := ibc.IBCTransferMsg{ + IBCPacket: packet, + } + + receiveMsg := ibc.IBCReceiveMsg{ + IBCPacket: packet, + Relayer: addr1, + Sequence: 0, + } + + SignCheckDeliver(t, bapp, transferMsg, 0, true) + CheckBalance(t, bapp, "") + SignCheckDeliver(t, bapp, transferMsg, 1, false) + SignCheckDeliver(t, bapp, receiveMsg, 2, true) + CheckBalance(t, bapp, "10foocoin") + SignCheckDeliver(t, bapp, receiveMsg, 3, false) +} + +// TODO describe the use of this function +func SignCheckDeliver(t *testing.T, bapp *DemocoinApp, msg sdk.Msg, seq int64, expPass bool) { + + // Sign the tx + tx := sdk.NewStdTx(msg, fee, []sdk.StdSignature{{ + PubKey: priv1.PubKey(), + Signature: priv1.Sign(sdk.StdSignBytes(chainID, []int64{seq}, fee, msg)), + Sequence: seq, + }}) + + // Run a Check + res := bapp.Check(tx) + if expPass { + require.Equal(t, sdk.CodeOK, res.Code, res.Log) + } else { + require.NotEqual(t, sdk.CodeOK, res.Code, res.Log) + } + + // Simulate a Block + bapp.BeginBlock(abci.RequestBeginBlock{}) + res = bapp.Deliver(tx) + if expPass { + require.Equal(t, sdk.CodeOK, res.Code, res.Log) + } else { + require.NotEqual(t, sdk.CodeOK, res.Code, res.Log) + } + bapp.EndBlock(abci.RequestEndBlock{}) + //bapp.Commit() +} + +func CheckBalance(t *testing.T, bapp *DemocoinApp, balExpected string) { + ctxDeliver := bapp.BaseApp.NewContext(false, abci.Header{}) + res2 := bapp.accountMapper.GetAccount(ctxDeliver, addr1) + assert.Equal(t, balExpected, fmt.Sprintf("%v", res2.GetCoins())) +} diff --git a/examples/democoin/cmd/democli/main.go b/examples/democoin/cmd/democli/main.go new file mode 100644 index 000000000..602e5478e --- /dev/null +++ b/examples/democoin/cmd/democli/main.go @@ -0,0 +1,91 @@ +package main + +import ( + "errors" + "os" + + "github.com/spf13/cobra" + + "github.com/tendermint/tmlibs/cli" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/client/keys" + "github.com/cosmos/cosmos-sdk/client/lcd" + "github.com/cosmos/cosmos-sdk/client/rpc" + "github.com/cosmos/cosmos-sdk/client/tx" + + "github.com/cosmos/cosmos-sdk/version" + authcmd "github.com/cosmos/cosmos-sdk/x/auth/commands" + bankcmd "github.com/cosmos/cosmos-sdk/x/bank/commands" + ibccmd "github.com/cosmos/cosmos-sdk/x/ibc/commands" + simplestakingcmd "github.com/cosmos/cosmos-sdk/x/simplestake/commands" + + "github.com/cosmos/cosmos-sdk/examples/democoin/app" + "github.com/cosmos/cosmos-sdk/examples/democoin/types" +) + +// gaiacliCmd is the entry point for this binary +var ( + democliCmd = &cobra.Command{ + Use: "democli", + Short: "Democoin light-client", + } +) + +func todoNotImplemented(_ *cobra.Command, _ []string) error { + return errors.New("TODO: Command not yet implemented") +} + +func main() { + // disable sorting + cobra.EnableCommandSorting = false + + // get the codec + cdc := app.MakeCodec() + + // TODO: setup keybase, viper object, etc. to be passed into + // the below functions and eliminate global vars, like we do + // with the cdc + + // add standard rpc, and tx commands + rpc.AddCommands(democliCmd) + democliCmd.AddCommand(client.LineBreak) + tx.AddCommands(democliCmd, cdc) + democliCmd.AddCommand(client.LineBreak) + + // add query/post commands (custom to binary) + democliCmd.AddCommand( + client.GetCommands( + authcmd.GetAccountCmd("main", cdc, types.GetAccountDecoder(cdc)), + )...) + democliCmd.AddCommand( + client.PostCommands( + bankcmd.SendTxCmd(cdc), + )...) + democliCmd.AddCommand( + client.PostCommands( + ibccmd.IBCTransferCmd(cdc), + )...) + democliCmd.AddCommand( + client.PostCommands( + ibccmd.IBCRelayCmd(cdc), + simplestakingcmd.BondTxCmd(cdc), + )...) + democliCmd.AddCommand( + client.PostCommands( + simplestakingcmd.UnbondTxCmd(cdc), + )...) + + // add proxy, version and key info + democliCmd.AddCommand( + client.LineBreak, + lcd.ServeCommand(cdc), + keys.Commands(), + client.LineBreak, + version.VersionCmd, + ) + + // prepare and add flags + executor := cli.PrepareMainCmd(democliCmd, "BC", os.ExpandEnv("$HOME/.democli")) + executor.Execute() +} diff --git a/examples/democoin/cmd/democoind/main.go b/examples/democoin/cmd/democoind/main.go new file mode 100644 index 000000000..d9421954c --- /dev/null +++ b/examples/democoin/cmd/democoind/main.go @@ -0,0 +1,99 @@ +package main + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + + "github.com/spf13/cobra" + + abci "github.com/tendermint/abci/types" + "github.com/tendermint/tmlibs/cli" + cmn "github.com/tendermint/tmlibs/common" + dbm "github.com/tendermint/tmlibs/db" + "github.com/tendermint/tmlibs/log" + + "github.com/cosmos/cosmos-sdk/examples/democoin/app" + "github.com/cosmos/cosmos-sdk/server" + "github.com/cosmos/cosmos-sdk/version" +) + +// democoindCmd is the entry point for this binary +var ( + democoindCmd = &cobra.Command{ + Use: "democoind", + Short: "Gaia Daemon (server)", + } +) + +// defaultOptions sets up the app_options for the +// default genesis file +func defaultOptions(args []string) (json.RawMessage, string, cmn.HexBytes, error) { + addr, secret, err := server.GenerateCoinKey() + if err != nil { + return nil, "", nil, err + } + fmt.Println("Secret phrase to access coins:") + fmt.Println(secret) + + opts := fmt.Sprintf(`{ + "accounts": [{ + "address": "%s", + "coins": [ + { + "denom": "mycoin", + "amount": 9007199254740992 + } + ] + }] + }`, addr) + return json.RawMessage(opts), "", nil, nil +} + +func generateApp(rootDir string, logger log.Logger) (abci.Application, error) { + dbMain, err := dbm.NewGoLevelDB("democoin", filepath.Join(rootDir, "data")) + if err != nil { + return nil, err + } + dbAcc, err := dbm.NewGoLevelDB("democoin-acc", filepath.Join(rootDir, "data")) + if err != nil { + return nil, err + } + dbIBC, err := dbm.NewGoLevelDB("democoin-ibc", filepath.Join(rootDir, "data")) + if err != nil { + return nil, err + } + dbStaking, err := dbm.NewGoLevelDB("democoin-staking", filepath.Join(rootDir, "data")) + if err != nil { + return nil, err + } + dbs := map[string]dbm.DB{ + "main": dbMain, + "acc": dbAcc, + "ibc": dbIBC, + "staking": dbStaking, + } + bapp := app.NewDemocoinApp(logger, dbs) + return bapp, nil +} + +func main() { + // TODO: set logger through CLI + logger := log.NewTMLogger(log.NewSyncWriter(os.Stdout)). + With("module", "main") + + democoindCmd.AddCommand( + server.InitCmd(defaultOptions, logger), + server.StartCmd(generateApp, logger), + server.UnsafeResetAllCmd(logger), + server.ShowNodeIdCmd(logger), + server.ShowValidatorCmd(logger), + version.VersionCmd, + ) + + // prepare and add flags + rootDir := os.ExpandEnv("$HOME/.democoind") + executor := cli.PrepareBaseCmd(democoindCmd, "BC", rootDir) + executor.Execute() +} diff --git a/examples/democoin/types/account.go b/examples/democoin/types/account.go new file mode 100644 index 000000000..f34113fc6 --- /dev/null +++ b/examples/democoin/types/account.go @@ -0,0 +1,72 @@ +package types + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/wire" + "github.com/cosmos/cosmos-sdk/x/auth" +) + +var _ sdk.Account = (*AppAccount)(nil) + +// Custom extensions for this application. This is just an example of +// extending auth.BaseAccount with custom fields. +// +// This is compatible with the stock auth.AccountStore, since +// auth.AccountStore uses the flexible go-wire library. +type AppAccount struct { + auth.BaseAccount + Name string `json:"name"` +} + +// nolint +func (acc AppAccount) GetName() string { return acc.Name } +func (acc *AppAccount) SetName(name string) { acc.Name = name } + +// Get the AccountDecoder function for the custom AppAccount +func GetAccountDecoder(cdc *wire.Codec) sdk.AccountDecoder { + return func(accBytes []byte) (res sdk.Account, err error) { + if len(accBytes) == 0 { + return nil, sdk.ErrTxDecode("accBytes are empty") + } + acct := new(AppAccount) + err = cdc.UnmarshalBinary(accBytes, &acct) + if err != nil { + panic(err) + } + return acct, err + } +} + +//___________________________________________________________________________________ + +// State to Unmarshal +type GenesisState struct { + Accounts []*GenesisAccount `json:"accounts"` +} + +// GenesisAccount doesn't need pubkey or sequence +type GenesisAccount struct { + Name string `json:"name"` + Address sdk.Address `json:"address"` + Coins sdk.Coins `json:"coins"` +} + +func NewGenesisAccount(aa *AppAccount) *GenesisAccount { + return &GenesisAccount{ + Name: aa.Name, + Address: aa.Address, + Coins: aa.Coins, + } +} + +// convert GenesisAccount to AppAccount +func (ga *GenesisAccount) ToAppAccount() (acc *AppAccount, err error) { + baseAcc := auth.BaseAccount{ + Address: ga.Address, + Coins: ga.Coins, + } + return &AppAccount{ + BaseAccount: baseAcc, + Name: ga.Name, + }, nil +} diff --git a/examples/basecoin/x/cool/commands/tx.go b/examples/democoin/x/cool/commands/tx.go similarity index 75% rename from examples/basecoin/x/cool/commands/tx.go rename to examples/democoin/x/cool/commands/tx.go index f06eb8af4..ab817309c 100644 --- a/examples/basecoin/x/cool/commands/tx.go +++ b/examples/democoin/x/cool/commands/tx.go @@ -11,7 +11,7 @@ import ( "github.com/cosmos/cosmos-sdk/client/builder" "github.com/cosmos/cosmos-sdk/wire" - "github.com/cosmos/cosmos-sdk/examples/basecoin/x/cool" + "github.com/cosmos/cosmos-sdk/examples/democoin/x/cool" ) // take the coolness quiz transaction @@ -36,16 +36,8 @@ func QuizTxCmd(cdc *wire.Codec) *cobra.Command { // get account name name := viper.GetString(client.FlagName) - // get password - buf := client.BufferStdin() - prompt := fmt.Sprintf("Password to sign with '%s':", name) - passphrase, err := client.GetPassword(prompt, buf) - if err != nil { - return err - } - // build and sign the transaction, then broadcast to Tendermint - res, err := builder.SignBuildBroadcast(name, passphrase, msg, cdc) + res, err := builder.SignBuildBroadcast(name, msg, cdc) if err != nil { return err } @@ -75,19 +67,11 @@ func SetTrendTxCmd(cdc *wire.Codec) *cobra.Command { // get account name name := viper.GetString(client.FlagName) - // get password - buf := client.BufferStdin() - prompt := fmt.Sprintf("Password to sign with '%s':", name) - passphrase, err := client.GetPassword(prompt, buf) - if err != nil { - return err - } - // create the message msg := cool.NewSetTrendMsg(from, args[0]) // build and sign the transaction, then broadcast to Tendermint - res, err := builder.SignBuildBroadcast(name, passphrase, msg, cdc) + res, err := builder.SignBuildBroadcast(name, msg, cdc) if err != nil { return err } diff --git a/examples/democoin/x/cool/errors.go b/examples/democoin/x/cool/errors.go new file mode 100644 index 000000000..5db3efc39 --- /dev/null +++ b/examples/democoin/x/cool/errors.go @@ -0,0 +1,17 @@ +package cool + +import ( + "fmt" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +const ( + // Cool module reserves error 400-499 lawl + CodeIncorrectCoolAnswer sdk.CodeType = 400 +) + +// ErrIncorrectCoolAnswer - Error returned upon an incorrect guess +func ErrIncorrectCoolAnswer(answer string) sdk.Error { + return sdk.NewError(CodeIncorrectCoolAnswer, fmt.Sprintf("Incorrect cool answer: %v", answer)) +} diff --git a/examples/basecoin/x/cool/handler.go b/examples/democoin/x/cool/handler.go similarity index 56% rename from examples/basecoin/x/cool/handler.go rename to examples/democoin/x/cool/handler.go index c0aae11bb..ce86ecd5b 100644 --- a/examples/basecoin/x/cool/handler.go +++ b/examples/democoin/x/cool/handler.go @@ -5,7 +5,6 @@ import ( "reflect" sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/cosmos/cosmos-sdk/x/bank" ) // This is just an example to demonstrate a functional custom module @@ -18,14 +17,14 @@ import ( //| $$$$$$$| $$$$$$/| $$$$$$/| $$$$$$$ // \_______/ \______/ \______/ |______/ -// Handle all "coolmodule" type objects -func NewHandler(ck bank.CoinKeeper, cm Mapper) sdk.Handler { +// NewHandler returns a handler for "cool" type messages. +func NewHandler(k Keeper) sdk.Handler { return func(ctx sdk.Context, msg sdk.Msg) sdk.Result { switch msg := msg.(type) { case SetTrendMsg: - return handleSetTrendMsg(ctx, cm, msg) + return handleSetTrendMsg(ctx, k, msg) case QuizMsg: - return handleQuizMsg(ctx, ck, cm, msg) + return handleQuizMsg(ctx, k, msg) default: errMsg := fmt.Sprintf("Unrecognized cool Msg type: %v", reflect.TypeOf(msg).Name()) return sdk.ErrUnknownRequest(errMsg).Result() @@ -34,22 +33,29 @@ func NewHandler(ck bank.CoinKeeper, cm Mapper) sdk.Handler { } // Handle QuizMsg This is the engine of your module -func handleSetTrendMsg(ctx sdk.Context, cm Mapper, msg SetTrendMsg) sdk.Result { - cm.SetTrend(ctx, msg.Cool) +func handleSetTrendMsg(ctx sdk.Context, k Keeper, msg SetTrendMsg) sdk.Result { + k.setTrend(ctx, msg.Cool) return sdk.Result{} } // Handle QuizMsg This is the engine of your module -func handleQuizMsg(ctx sdk.Context, ck bank.CoinKeeper, cm Mapper, msg QuizMsg) sdk.Result { +func handleQuizMsg(ctx sdk.Context, k Keeper, msg QuizMsg) sdk.Result { - currentTrend := cm.GetTrend(ctx) + correct := k.CheckTrend(ctx, msg.CoolAnswer) - if msg.CoolAnswer == currentTrend { - bonusCoins := sdk.Coins{{currentTrend, 69}} - _, err := ck.AddCoins(ctx, msg.Sender, bonusCoins) - if err != nil { - return err.Result() - } + if !correct { + return ErrIncorrectCoolAnswer(msg.CoolAnswer).Result() + } + + if ctx.IsCheckTx() { + return sdk.Result{} // TODO + } + + bonusCoins := sdk.Coins{{msg.CoolAnswer, 69}} + + _, err := k.ck.AddCoins(ctx, msg.Sender, bonusCoins) + if err != nil { + return err.Result() } return sdk.Result{} diff --git a/examples/democoin/x/cool/keeper.go b/examples/democoin/x/cool/keeper.go new file mode 100644 index 000000000..1bf342fdc --- /dev/null +++ b/examples/democoin/x/cool/keeper.go @@ -0,0 +1,59 @@ +package cool + +import ( + "encoding/json" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/bank" +) + +// Cool genesis state, containing the genesis trend +type GenesisState struct { + trend string +} + +// Keeper - handlers sets/gets of custom variables for your module +type Keeper struct { + ck bank.CoinKeeper + + storeKey sdk.StoreKey // The (unexposed) key used to access the store from the Context. +} + +// NewKeeper - Returns the Keeper +func NewKeeper(key sdk.StoreKey, bankKeeper bank.CoinKeeper) Keeper { + return Keeper{bankKeeper, key} +} + +// Key to knowing the trend on the streets! +var trendKey = []byte("TrendKey") + +// GetTrend - returns the current cool trend +func (k Keeper) GetTrend(ctx sdk.Context) string { + store := ctx.KVStore(k.storeKey) + bz := store.Get(trendKey) + return string(bz) +} + +// Implements sdk.AccountMapper. +func (k Keeper) setTrend(ctx sdk.Context, newTrend string) { + store := ctx.KVStore(k.storeKey) + store.Set(trendKey, []byte(newTrend)) +} + +// CheckTrend - Returns true or false based on whether guessedTrend is currently cool or not +func (k Keeper) CheckTrend(ctx sdk.Context, guessedTrend string) bool { + if guessedTrend == k.GetTrend(ctx) { + return true + } + return false +} + +// InitGenesis - store the genesis trend +func (k Keeper) InitGenesis(ctx sdk.Context, data json.RawMessage) error { + var state GenesisState + if err := json.Unmarshal(data, &state); err != nil { + return err + } + k.setTrend(ctx, state.trend) + return nil +} diff --git a/examples/basecoin/x/cool/types.go b/examples/democoin/x/cool/types.go similarity index 100% rename from examples/basecoin/x/cool/types.go rename to examples/democoin/x/cool/types.go diff --git a/examples/basecoin/x/sketchy/handler.go b/examples/democoin/x/sketchy/handler.go similarity index 100% rename from examples/basecoin/x/sketchy/handler.go rename to examples/democoin/x/sketchy/handler.go diff --git a/examples/gaia/README.md b/examples/gaia/README.md new file mode 100644 index 000000000..485af236f --- /dev/null +++ b/examples/gaia/README.md @@ -0,0 +1,3 @@ +Gaiad is the abci application, which can be run stand-alone, or in-process with tendermint. + +Gaiacli is a client application, which connects to tendermint rpc, and sends transactions and queries the state. It uses light-client proofs to guarantee the results even if it doesn't have 100% trust in the node it connects to. diff --git a/examples/gaia/gaiacli/client.go b/examples/gaia/gaiacli/client.go new file mode 100644 index 000000000..682a571e6 --- /dev/null +++ b/examples/gaia/gaiacli/client.go @@ -0,0 +1,131 @@ +package main + +import "github.com/spf13/cobra" + +const ( + // these are needed for every init + flagChainID = "chain-id" + flagNode = "node" + + // one of the following should be provided to verify the connection + flagGenesis = "genesis" + flagCommit = "commit" + flagValHash = "validator-set" + + flagSelect = "select" + flagTags = "tag" + flagAny = "any" + + flagBind = "bind" + flagCORS = "cors" + flagTrustNode = "trust-node" + + // this is for signing + flagName = "name" +) + +var ( + statusCmd = &cobra.Command{ + Use: "status", + Short: "Query remote node for status", + RunE: todoNotImplemented, + } +) + +// AddClientCommands returns a sub-tree of all basic client commands +// +// Call AddGetCommand and AddPostCommand to add custom txs and queries +func AddClientCommands(cmd *cobra.Command) { + cmd.AddCommand( + initClientCommand(), + statusCmd, + blockCommand(), + validatorCommand(), + lineBreak, + txSearchCommand(), + txCommand(), + lineBreak, + ) +} + +// GetCommands adds common flags to query commands +func GetCommands(cmds ...*cobra.Command) []*cobra.Command { + for _, c := range cmds { + c.Flags().Bool(flagTrustNode, false, "Don't verify proofs for responses") + } + return cmds +} + +// PostCommands adds common flags for commands to post tx +func PostCommands(cmds ...*cobra.Command) []*cobra.Command { + for _, c := range cmds { + c.Flags().String(flagName, "", "Name of private key with which to sign") + c.Flags().String(flagPassword, "", "Password to use the named private key") + } + return cmds +} + +func initClientCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "init", + Short: "Initialize light client", + RunE: todoNotImplemented, + } + cmd.Flags().StringP(flagChainID, "c", "", "ID of chain we connect to") + cmd.Flags().StringP(flagNode, "n", "tcp://localhost:46657", "Node to connect to") + cmd.Flags().String(flagGenesis, "", "Genesis file to verify header validity") + cmd.Flags().String(flagCommit, "", "File with trusted and signed header") + cmd.Flags().String(flagValHash, "", "Hash of trusted validator set (hex-encoded)") + return cmd +} + +func blockCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "block ", + Short: "Get verified data for a the block at given height", + RunE: todoNotImplemented, + } + cmd.Flags().StringSlice(flagSelect, []string{"header", "tx"}, "Fields to return (header|txs|results)") + return cmd +} + +func validatorCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "validatorset ", + Short: "Get the full validator set at given height", + RunE: todoNotImplemented, + } + return cmd +} + +func serveCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "serve", + Short: "Start LCD (light-client daemon), a local REST server", + RunE: todoNotImplemented, + } + // TODO: handle unix sockets also? + cmd.Flags().StringP(flagBind, "b", "localhost:1317", "Interface and port that server binds to") + cmd.Flags().String(flagCORS, "", "Set to domains that can make CORS requests (* for all)") + return cmd +} + +func txSearchCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "txs", + Short: "Search for all transactions that match the given tags", + RunE: todoNotImplemented, + } + cmd.Flags().StringSlice(flagTags, nil, "Tags that must match (may provide multiple)") + cmd.Flags().Bool(flagAny, false, "Return transactions that match ANY tag, rather than ALL") + return cmd +} + +func txCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "tx ", + Short: "Matches this txhash over all committed blocks", + RunE: todoNotImplemented, + } + return cmd +} diff --git a/examples/gaia/gaiacli/key.go b/examples/gaia/gaiacli/key.go new file mode 100644 index 000000000..b3ffa323a --- /dev/null +++ b/examples/gaia/gaiacli/key.go @@ -0,0 +1,77 @@ +package main + +import "github.com/spf13/cobra" + +const ( + flagPassword = "password" + flagNewPassword = "new-password" + flagType = "type" + flagSeed = "seed" + flagDryRun = "dry-run" +) + +var ( + listKeysCmd = &cobra.Command{ + Use: "list", + Short: "List all locally availably keys", + RunE: todoNotImplemented, + } + + showKeysCmd = &cobra.Command{ + Use: "show ", + Short: "Show key info for the given name", + RunE: todoNotImplemented, + } +) + +// KeyCommands registers a sub-tree of commands to interact with +// local private key storage. +func KeyCommands() *cobra.Command { + cmd := &cobra.Command{ + Use: "keys", + Short: "Add or view local private keys", + } + cmd.AddCommand( + addKeyCommand(), + listKeysCmd, + showKeysCmd, + lineBreak, + deleteKeyCommand(), + updateKeyCommand(), + ) + return cmd +} + +func addKeyCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "add ", + Short: "Create a new key, or import from seed", + RunE: todoNotImplemented, + } + cmd.Flags().StringP(flagPassword, "p", "", "Password to encrypt private key") + cmd.Flags().StringP(flagType, "t", "ed25519", "Type of private key (ed25519|secp256k1|ledger)") + cmd.Flags().StringP(flagSeed, "s", "", "Provide seed phrase to recover existing key instead of creating") + cmd.Flags().Bool(flagDryRun, false, "Perform action, but don't add key to local keystore") + return cmd +} + +func updateKeyCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "update ", + Short: "Change the password used to protect private key", + RunE: todoNotImplemented, + } + cmd.Flags().StringP(flagPassword, "p", "", "Current password to decrypt key") + cmd.Flags().String(flagNewPassword, "", "New password to use to protect key") + return cmd +} + +func deleteKeyCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "delete ", + Short: "Delete the given key", + RunE: todoNotImplemented, + } + cmd.Flags().StringP(flagPassword, "p", "", "Password of existing key to delete") + return cmd +} diff --git a/examples/gaia/gaiacli/main.go b/examples/gaia/gaiacli/main.go new file mode 100644 index 000000000..dce125acb --- /dev/null +++ b/examples/gaia/gaiacli/main.go @@ -0,0 +1,77 @@ +package main + +import ( + "errors" + "os" + + "github.com/spf13/cobra" + + "github.com/tendermint/tmlibs/cli" + + "github.com/cosmos/cosmos-sdk/version" +) + +const ( + flagTo = "to" + flagAmount = "amount" + flagFee = "fee" +) + +// gaiacliCmd is the entry point for this binary +var ( + gaiacliCmd = &cobra.Command{ + Use: "gaiacli", + Short: "Gaia light-client", + } + + lineBreak = &cobra.Command{Run: func(*cobra.Command, []string) {}} + + getAccountCmd = &cobra.Command{ + Use: "account
", + Short: "Query account balance", + RunE: todoNotImplemented, + } +) + +func todoNotImplemented(_ *cobra.Command, _ []string) error { + return errors.New("TODO: Command not yet implemented") +} + +func postSendCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "send", + Short: "Create and sign a send tx", + RunE: todoNotImplemented, + } + cmd.Flags().String(flagTo, "", "Address to send coins") + cmd.Flags().String(flagAmount, "", "Amount of coins to send") + cmd.Flags().String(flagFee, "", "Fee to pay along with transaction") + return cmd +} + +func main() { + // disable sorting + cobra.EnableCommandSorting = false + + // generic client commands + AddClientCommands(gaiacliCmd) + // query commands (custom to binary) + gaiacliCmd.AddCommand( + GetCommands(getAccountCmd)...) + // post tx commands (custom to binary) + gaiacliCmd.AddCommand( + PostCommands(postSendCommand())...) + + // add proxy, version and key info + gaiacliCmd.AddCommand( + lineBreak, + serveCommand(), + KeyCommands(), + lineBreak, + version.VersionCmd, + ) + + // prepare and add flags + executor := cli.PrepareBaseCmd(gaiacliCmd, "GA", os.ExpandEnv("$HOME/.gaiacli")) + executor.Execute() +} diff --git a/examples/gaia/gaiad/main.go b/examples/gaia/gaiad/main.go new file mode 100644 index 000000000..70a44d8cb --- /dev/null +++ b/examples/gaia/gaiad/main.go @@ -0,0 +1,72 @@ +package main + +import ( + "encoding/json" + "fmt" + "os" + + "github.com/spf13/cobra" + + abci "github.com/tendermint/abci/types" + "github.com/tendermint/tmlibs/cli" + cmn "github.com/tendermint/tmlibs/common" + "github.com/tendermint/tmlibs/log" + + "github.com/cosmos/cosmos-sdk/baseapp" + "github.com/cosmos/cosmos-sdk/server" + "github.com/cosmos/cosmos-sdk/version" +) + +// gaiadCmd is the entry point for this binary +var ( + gaiadCmd = &cobra.Command{ + Use: "gaiad", + Short: "Gaia Daemon (server)", + } +) + +// defaultOptions sets up the app_options for the +// default genesis file +func defaultOptions(args []string) (json.RawMessage, string, cmn.HexBytes, error) { + addr, secret, err := server.GenerateCoinKey() + if err != nil { + return nil, "", nil, err + } + fmt.Println("Secret phrase to access coins:") + fmt.Println(secret) + + opts := fmt.Sprintf(`{ + "accounts": [{ + "address": "%s", + "coins": [ + { + "denom": "mycoin", + "amount": 9007199254740992 + } + ] + }] + }`, addr) + return json.RawMessage(opts), secret, addr, nil +} + +func generateApp(rootDir string, logger log.Logger) (abci.Application, error) { + // TODO: set this to something real + app := new(baseapp.BaseApp) + return app, nil +} + +func main() { + logger := log.NewTMLogger(log.NewSyncWriter(os.Stdout)). + With("module", "main") + + gaiadCmd.AddCommand( + server.InitCmd(defaultOptions, logger), + server.StartCmd(generateApp, logger), + server.UnsafeResetAllCmd(logger), + version.VersionCmd, + ) + + // prepare and add flags + executor := cli.PrepareBaseCmd(gaiadCmd, "GA", os.ExpandEnv("$HOME/.gaiad")) + executor.Execute() +} diff --git a/examples/kvstore/main.go b/examples/kvstore/main.go index 5ffe590f1..0d80826ed 100644 --- a/examples/kvstore/main.go +++ b/examples/kvstore/main.go @@ -3,8 +3,12 @@ package main import ( "fmt" "os" + "path/filepath" + + "github.com/spf13/viper" "github.com/tendermint/abci/server" + "github.com/tendermint/tmlibs/cli" cmn "github.com/tendermint/tmlibs/common" dbm "github.com/tendermint/tmlibs/db" "github.com/tendermint/tmlibs/log" @@ -17,7 +21,8 @@ func main() { logger := log.NewTMLogger(log.NewSyncWriter(os.Stdout)).With("module", "main") - db, err := dbm.NewGoLevelDB("basecoind", "data") + rootDir := viper.GetString(cli.HomeFlag) + db, err := dbm.NewGoLevelDB("basecoind", filepath.Join(rootDir, "data")) if err != nil { fmt.Println(err) os.Exit(1) @@ -30,7 +35,7 @@ func main() { var baseApp = bam.NewBaseApp("kvstore", logger, db) // Set mounts for BaseApp's MultiStore. - baseApp.MountStore(capKeyMainStore, sdk.StoreTypeIAVL) + baseApp.MountStoresIAVL(capKeyMainStore) // Set Tx decoder baseApp.SetTxDecoder(decodeTx) diff --git a/mock/app.go b/mock/app.go index 3e9a3ad5f..eda490a8e 100644 --- a/mock/app.go +++ b/mock/app.go @@ -3,8 +3,10 @@ package mock import ( "encoding/json" "fmt" + "path/filepath" abci "github.com/tendermint/abci/types" + cmn "github.com/tendermint/tmlibs/common" dbm "github.com/tendermint/tmlibs/db" "github.com/tendermint/tmlibs/log" @@ -17,7 +19,7 @@ import ( // Make sure rootDir is empty before running the test, // in order to guarantee consistent results func NewApp(rootDir string, logger log.Logger) (abci.Application, error) { - db, err := dbm.NewGoLevelDB("mock", rootDir) + db, err := dbm.NewGoLevelDB("mock", filepath.Join(rootDir, "data")) if err != nil { return nil, err } @@ -105,7 +107,7 @@ func InitChainer(key sdk.StoreKey) func(sdk.Context, abci.RequestInitChain) abci // GenInitOptions can be passed into InitCmd, // returns a static string of a few key-values that can be parsed // by InitChainer -func GenInitOptions(args []string) (json.RawMessage, error) { +func GenInitOptions(args []string) (json.RawMessage, string, cmn.HexBytes, error) { opts := []byte(`{ "values": [ { @@ -118,5 +120,5 @@ func GenInitOptions(args []string) (json.RawMessage, error) { } ] }`) - return opts, nil + return opts, "", nil, nil } diff --git a/mock/app_test.go b/mock/app_test.go index 103530e50..47db93e1c 100644 --- a/mock/app_test.go +++ b/mock/app_test.go @@ -21,7 +21,7 @@ func TestInitApp(t *testing.T) { require.NoError(t, err) // initialize it future-way - opts, err := GenInitOptions(nil) + opts, _, _, err := GenInitOptions(nil) require.NoError(t, err) req := abci.RequestInitChain{AppStateBytes: opts} app.InitChain(req) diff --git a/server/init.go b/server/init.go index 12e330dbc..ab4ffcb7a 100644 --- a/server/init.go +++ b/server/init.go @@ -2,6 +2,7 @@ package server import ( "encoding/json" + "fmt" "io/ioutil" "github.com/spf13/cobra" @@ -11,9 +12,17 @@ import ( tcmd "github.com/tendermint/tendermint/cmd/tendermint/commands" cfg "github.com/tendermint/tendermint/config" + "github.com/tendermint/tendermint/p2p" tmtypes "github.com/tendermint/tendermint/types" ) +type testnetInformation struct { + Secret string `json:"secret"` + Account string `json:"account"` + Validator tmtypes.GenesisValidator `json:"validator"` + NodeID p2p.ID `json:"node_id"` +} + // InitCmd will initialize all files for tendermint, // along with proper app_state. // The application can pass in a function to generate @@ -24,17 +33,19 @@ func InitCmd(gen GenAppState, logger log.Logger) *cobra.Command { genAppState: gen, logger: logger, } - return &cobra.Command{ + cobraCmd := cobra.Command{ Use: "init", Short: "Initialize genesis files", RunE: cmd.run, } + return &cobraCmd } -// GenAppState can parse command-line and flag to +// GenAppState can parse command-line to // generate default app_state for the genesis file. +// Also must return generated seed and address // This is application-specific -type GenAppState func(args []string) (json.RawMessage, error) +type GenAppState func(args []string) (json.RawMessage, string, cmn.HexBytes, error) type initCmd struct { genAppState GenAppState @@ -42,13 +53,16 @@ type initCmd struct { } func (c initCmd) run(cmd *cobra.Command, args []string) error { + // Store testnet information as we go + var testnetInfo testnetInformation + // Run the basic tendermint initialization, // set up a default genesis with no app_options config, err := tcmd.ParseConfig() if err != nil { return err } - err = c.initTendermintFiles(config) + err = c.initTendermintFiles(config, &testnetInfo) if err != nil { return err } @@ -59,19 +73,36 @@ func (c initCmd) run(cmd *cobra.Command, args []string) error { } // Now, we want to add the custom app_state - appState, err := c.genAppState(args) + appState, secret, address, err := c.genAppState(args) if err != nil { return err } + testnetInfo.Secret = secret + testnetInfo.Account = address.String() + // And add them to the genesis file genFile := config.GenesisFile() - return addGenesisState(genFile, appState) + if err := addGenesisState(genFile, appState); err != nil { + return err + } + + nodeKey, err := p2p.LoadOrGenNodeKey(config.NodeKeyFile()) + if err != nil { + return err + } + testnetInfo.NodeID = nodeKey.ID() + out, err := json.MarshalIndent(testnetInfo, "", " ") + if err != nil { + return err + } + fmt.Println(string(out)) + return nil } // This was copied from tendermint/cmd/tendermint/commands/init.go // so we could pass in the config and the logger. -func (c initCmd) initTendermintFiles(config *cfg.Config) error { +func (c initCmd) initTendermintFiles(config *cfg.Config, info *testnetInformation) error { // private validator privValFile := config.PrivValidatorFile() var privValidator *tmtypes.PrivValidatorFS @@ -102,6 +133,18 @@ func (c initCmd) initTendermintFiles(config *cfg.Config) error { } c.logger.Info("Generated genesis file", "path", genFile) } + + // reload the config file and find our validator info + loadedDoc, err := tmtypes.GenesisDocFromFile(genFile) + if err != nil { + return err + } + for _, validator := range loadedDoc.Validators { + if validator.PubKey == privValidator.GetPubKey() { + info.Validator = validator + } + } + return nil } diff --git a/server/show_validator.go b/server/show_validator.go new file mode 100644 index 000000000..d2c7705d6 --- /dev/null +++ b/server/show_validator.go @@ -0,0 +1,40 @@ +package server + +import ( + "fmt" + + "github.com/spf13/cobra" + + "github.com/tendermint/go-wire/data" + tcmd "github.com/tendermint/tendermint/cmd/tendermint/commands" + "github.com/tendermint/tendermint/types" + "github.com/tendermint/tmlibs/log" +) + +// ShowValidator - ported from Tendermint, show this node's validator info +func ShowValidatorCmd(logger log.Logger) *cobra.Command { + cmd := showValidator{logger} + return &cobra.Command{ + Use: "show_validator", + Short: "Show this node's validator info", + RunE: cmd.run, + } +} + +type showValidator struct { + logger log.Logger +} + +func (s showValidator) run(cmd *cobra.Command, args []string) error { + cfg, err := tcmd.ParseConfig() + if err != nil { + return err + } + privValidator := types.LoadOrGenPrivValidatorFS(cfg.PrivValidatorFile()) + pubKeyJSONBytes, err := data.ToJSON(privValidator.PubKey) + if err != nil { + return err + } + fmt.Println(string(pubKeyJSONBytes)) + return nil +} diff --git a/types/account.go b/types/account.go index c0fadcd3c..91ad49979 100644 --- a/types/account.go +++ b/types/account.go @@ -1,6 +1,9 @@ package types import ( + "encoding/hex" + "errors" + crypto "github.com/tendermint/go-crypto" cmn "github.com/tendermint/tmlibs/common" ) @@ -8,6 +11,18 @@ import ( // Address in go-crypto style type Address = cmn.HexBytes +// create an Address from a string +func GetAddress(address string) (addr Address, err error) { + if len(address) == 0 { + return addr, errors.New("must use provide address") + } + bz, err := hex.DecodeString(address) + if err != nil { + return nil, err + } + return Address(bz), nil +} + // Account is a standard account using a sequence number for replay protection // and a pubkey for authentication. type Account interface { diff --git a/types/coin.go b/types/coin.go index 92871cd17..d19d4d854 100644 --- a/types/coin.go +++ b/types/coin.go @@ -19,6 +19,11 @@ func (coin Coin) String() string { return fmt.Sprintf("%v%v", coin.Amount, coin.Denom) } +// SameDenomAs returns true if the two coins are the same denom +func (coin Coin) SameDenomAs(other Coin) bool { + return (coin.Denom == other.Denom) +} + // IsZero returns if this represents no money func (coin Coin) IsZero() bool { return coin.Amount == 0 @@ -27,8 +32,38 @@ func (coin Coin) IsZero() bool { // IsGTE returns true if they are the same type and the receiver is // an equal or greater value func (coin Coin) IsGTE(other Coin) bool { - return (coin.Denom == other.Denom) && - (coin.Amount >= other.Amount) + return coin.SameDenomAs(other) && (coin.Amount >= other.Amount) +} + +// IsEqual returns true if the two sets of Coins have the same value +func (coin Coin) IsEqual(other Coin) bool { + return coin.SameDenomAs(other) && (coin.Amount == other.Amount) +} + +// IsPositive returns true if coin amount is positive +func (coin Coin) IsPositive() bool { + return (coin.Amount > 0) +} + +// IsNotNegative returns true if coin amount is not negative +func (coin Coin) IsNotNegative() bool { + return (coin.Amount >= 0) +} + +// Adds amounts of two coins with same denom +func (coin Coin) Plus(coinB Coin) Coin { + if !coin.SameDenomAs(coinB) { + return coin + } + return Coin{coin.Denom, coin.Amount + coinB.Amount} +} + +// Subtracts amounts of two coins with same denom +func (coin Coin) Minus(coinB Coin) Coin { + if !coin.SameDenomAs(coinB) { + return coin + } + return Coin{coin.Denom, coin.Amount - coinB.Amount} } //---------------------------------------- @@ -55,14 +90,14 @@ func (coins Coins) IsValid() bool { case 0: return true case 1: - return coins[0].Amount != 0 + return !coins[0].IsZero() default: lowDenom := coins[0].Denom for _, coin := range coins[1:] { if coin.Denom <= lowDenom { return false } - if coin.Amount == 0 { + if coin.IsZero() { return false } // we compare each coin against the last denom @@ -96,10 +131,7 @@ func (coins Coins) Plus(coinsB Coins) Coins { if coinA.Amount+coinB.Amount == 0 { // ignore 0 sum coin type } else { - sum = append(sum, Coin{ - Denom: coinA.Denom, - Amount: coinA.Amount + coinB.Amount, - }) + sum = append(sum, coinA.Plus(coinB)) } indexA++ indexB++ @@ -168,8 +200,8 @@ func (coins Coins) IsPositive() bool { if len(coins) == 0 { return false } - for _, coinAmount := range coins { - if coinAmount.Amount <= 0 { + for _, coin := range coins { + if !coin.IsPositive() { return false } } @@ -182,8 +214,8 @@ func (coins Coins) IsNotNegative() bool { if len(coins) == 0 { return true } - for _, coinAmount := range coins { - if coinAmount.Amount < 0 { + for _, coin := range coins { + if !coin.IsNotNegative() { return false } } diff --git a/types/coin_test.go b/types/coin_test.go index b58578a25..19929e8c7 100644 --- a/types/coin_test.go +++ b/types/coin_test.go @@ -6,6 +6,144 @@ import ( "github.com/stretchr/testify/assert" ) +func TestIsPositiveCoin(t *testing.T) { + assert := assert.New(t) + + cases := []struct { + inputOne Coin + expected bool + }{ + {Coin{"A", 1}, true}, + {Coin{"A", 0}, false}, + {Coin{"a", -1}, false}, + } + + for _, tc := range cases { + res := tc.inputOne.IsPositive() + assert.Equal(tc.expected, res) + } +} + +func TestIsNotNegativeCoin(t *testing.T) { + assert := assert.New(t) + + cases := []struct { + inputOne Coin + expected bool + }{ + {Coin{"A", 1}, true}, + {Coin{"A", 0}, true}, + {Coin{"a", -1}, false}, + } + + for _, tc := range cases { + res := tc.inputOne.IsNotNegative() + assert.Equal(tc.expected, res) + } +} + +func TestSameDenomAsCoin(t *testing.T) { + assert := assert.New(t) + + cases := []struct { + inputOne Coin + inputTwo Coin + expected bool + }{ + {Coin{"A", 1}, Coin{"A", 1}, true}, + {Coin{"A", 1}, Coin{"a", 1}, false}, + {Coin{"a", 1}, Coin{"b", 1}, false}, + {Coin{"steak", 1}, Coin{"steak", 10}, true}, + {Coin{"steak", -11}, Coin{"steak", 10}, true}, + } + + for _, tc := range cases { + res := tc.inputOne.SameDenomAs(tc.inputTwo) + assert.Equal(tc.expected, res) + } +} + +func TestIsGTECoin(t *testing.T) { + assert := assert.New(t) + + cases := []struct { + inputOne Coin + inputTwo Coin + expected bool + }{ + {Coin{"A", 1}, Coin{"A", 1}, true}, + {Coin{"A", 2}, Coin{"A", 1}, true}, + {Coin{"A", -1}, Coin{"A", 5}, false}, + {Coin{"a", 1}, Coin{"b", 1}, false}, + } + + for _, tc := range cases { + res := tc.inputOne.IsGTE(tc.inputTwo) + assert.Equal(tc.expected, res) + } +} + +func TestIsEqualCoin(t *testing.T) { + assert := assert.New(t) + + cases := []struct { + inputOne Coin + inputTwo Coin + expected bool + }{ + {Coin{"A", 1}, Coin{"A", 1}, true}, + {Coin{"A", 1}, Coin{"a", 1}, false}, + {Coin{"a", 1}, Coin{"b", 1}, false}, + {Coin{"steak", 1}, Coin{"steak", 10}, false}, + {Coin{"steak", -11}, Coin{"steak", 10}, false}, + } + + for _, tc := range cases { + res := tc.inputOne.IsEqual(tc.inputTwo) + assert.Equal(tc.expected, res) + } +} + +func TestPlusCoin(t *testing.T) { + assert := assert.New(t) + + cases := []struct { + inputOne Coin + inputTwo Coin + expected Coin + }{ + {Coin{"A", 1}, Coin{"A", 1}, Coin{"A", 2}}, + {Coin{"A", 1}, Coin{"B", 1}, Coin{"A", 1}}, + {Coin{"asdf", -4}, Coin{"asdf", 5}, Coin{"asdf", 1}}, + {Coin{"asdf", -1}, Coin{"asdf", 1}, Coin{"asdf", 0}}, + } + + for _, tc := range cases { + res := tc.inputOne.Plus(tc.inputTwo) + assert.Equal(tc.expected, res) + } +} + +func TestMinusCoin(t *testing.T) { + assert := assert.New(t) + + cases := []struct { + inputOne Coin + inputTwo Coin + expected Coin + }{ + {Coin{"A", 1}, Coin{"A", 1}, Coin{"A", 0}}, + {Coin{"A", 1}, Coin{"B", 1}, Coin{"A", 1}}, + {Coin{"asdf", -4}, Coin{"asdf", 5}, Coin{"asdf", -9}}, + {Coin{"asdf", 10}, Coin{"asdf", 1}, Coin{"asdf", 9}}, + } + + for _, tc := range cases { + res := tc.inputOne.Minus(tc.inputTwo) + assert.Equal(tc.expected, res) + } +} + func TestCoins(t *testing.T) { //Define the coins to be used in tests diff --git a/types/errors.go b/types/errors.go index fab28c444..1115d3937 100644 --- a/types/errors.go +++ b/types/errors.go @@ -21,7 +21,7 @@ func (code CodeType) IsOK() bool { const ( CodeOK CodeType = 0 CodeInternal CodeType = 1 - CodeTxDecode CodeType = 2 + CodeTxDecode CodeType = 2 CodeInvalidSequence CodeType = 3 CodeUnauthorized CodeType = 4 CodeInsufficientFunds CodeType = 5 @@ -32,7 +32,7 @@ const ( CodeInsufficientCoins CodeType = 10 CodeInvalidCoins CodeType = 11 - CodeGenesisParse CodeType = 0xdead // TODO: remove ? + CodeGenesisParse CodeType = 0xdead // TODO: remove ? // why remove? ) // NOTE: Don't stringer this, we'll put better messages in later. diff --git a/types/initgenesis.go b/types/initgenesis.go new file mode 100644 index 000000000..ff3c03cf4 --- /dev/null +++ b/types/initgenesis.go @@ -0,0 +1,10 @@ +package types + +import ( + "encoding/json" +) + +// Run only once on chain initialization, should write genesis state to store +// or throw an error if some required information was not provided, in which case +// the application will panic. +type InitGenesis func(ctx Context, data json.RawMessage) error diff --git a/types/rational.go b/types/rational.go new file mode 100644 index 000000000..8ebee7140 --- /dev/null +++ b/types/rational.go @@ -0,0 +1,237 @@ +package types + +import ( + "fmt" + "math/big" + "strconv" + "strings" +) + +// "that's one big rat!" +// ______ +// / / /\ \____oo +// __ /___...._____ _\o +// __| |_ |_ + +// Rat - extend big.Rat +// NOTE: never use new(Rat) or else +// we will panic unmarshalling into the +// nil embedded big.Rat +type Rat struct { + Num int64 `json:"num"` + Denom int64 `json:"denom"` + //*big.Rat `json:"rat"` +} + +// RatInterface - big Rat with additional functionality +// NOTE: we only have one implementation of this interface +// and don't use it anywhere, but it might come in handy +// if we want to provide Rat types that include +// the units of the value in the type system. +//type RatInterface interface { +//GetRat() *big.Rat +//Num() int64 +//Denom() int64 +//GT(Rat) bool +//LT(Rat) bool +//Equal(Rat) bool +//IsZero() bool +//Inv() Rat +//Mul(Rat) Rat +//Quo(Rat) Rat +//Add(Rat) Rat +//Sub(Rat) Rat +//Round(int64) Rat +//Evaluate() int64 +//} +//var _ Rat = Rat{} // enforce at compile time + +// nolint - common values +var ( + ZeroRat = NewRat(0) // Rat{big.NewRat(0, 1)} + OneRat = NewRat(1) // Rat{big.NewRat(1, 1)} +) + +// New - create a new Rat from integers +//func NewRat(Numerator int64, Denominator ...int64) Rat { +//switch len(Denominator) { +//case 0: +//return Rat{big.NewRat(Numerator, 1)} +//case 1: +//return Rat{big.NewRat(Numerator, Denominator[0])} +//default: +//panic("improper use of New, can only have one denominator") +//} +//} +func NewRat(num int64, denom ...int64) Rat { + switch len(denom) { + case 0: + return Rat{ + Num: num, + Denom: 1, + } + case 1: + return Rat{ + Num: num, + Denom: denom[0], + } + default: + panic("improper use of New, can only have one denominator") + } +} + +// create a rational from decimal string or integer string +func NewRatFromDecimal(decimalStr string) (f Rat, err Error) { + + // first extract any negative symbol + neg := false + if string(decimalStr[0]) == "-" { + neg = true + decimalStr = decimalStr[1:] + } + + str := strings.Split(decimalStr, ".") + + var numStr string + var denom int64 = 1 + switch len(str) { + case 1: + if len(str[0]) == 0 { + return f, NewError(CodeUnknownRequest, "not a decimal string") + } + numStr = str[0] + case 2: + if len(str[0]) == 0 || len(str[1]) == 0 { + return f, NewError(CodeUnknownRequest, "not a decimal string") + } + numStr = str[0] + str[1] + len := int64(len(str[1])) + denom = new(big.Int).Exp(big.NewInt(10), big.NewInt(len), nil).Int64() + default: + return f, NewError(CodeUnknownRequest, "not a decimal string") + } + + num, errConv := strconv.Atoi(numStr) + if errConv != nil { + return f, NewError(CodeUnknownRequest, errConv.Error()) + } + + if neg { + num *= -1 + } + + return NewRat(int64(num), denom), nil +} + +//nolint +func ToRat(r *big.Rat) Rat { return NewRat(r.Num().Int64(), r.Denom().Int64()) } // GetRat - get big.Rat +func (r Rat) GetRat() *big.Rat { return big.NewRat(r.Num, r.Denom) } // GetRat - get big.Rat +func (r Rat) IsZero() bool { return r.Num == 0 } // IsZero - Is the Rat equal to zero +func (r Rat) Equal(r2 Rat) bool { return r.GetRat().Cmp(r2.GetRat()) == 0 } // Equal - rationals are equal +func (r Rat) GT(r2 Rat) bool { return r.GetRat().Cmp(r2.GetRat()) == 1 } // GT - greater than +func (r Rat) LT(r2 Rat) bool { return r.GetRat().Cmp(r2.GetRat()) == -1 } // LT - less than +func (r Rat) Inv() Rat { return ToRat(new(big.Rat).Inv(r.GetRat())) } // Inv - inverse +func (r Rat) Mul(r2 Rat) Rat { return ToRat(new(big.Rat).Mul(r.GetRat(), r2.GetRat())) } // Mul - multiplication +func (r Rat) Quo(r2 Rat) Rat { return ToRat(new(big.Rat).Quo(r.GetRat(), r2.GetRat())) } // Quo - quotient +func (r Rat) Add(r2 Rat) Rat { return ToRat(new(big.Rat).Add(r.GetRat(), r2.GetRat())) } // Add - addition +func (r Rat) Sub(r2 Rat) Rat { return ToRat(new(big.Rat).Sub(r.GetRat(), r2.GetRat())) } // Sub - subtraction +//func (r Rat) GetRat() *big.Rat { return r.Rat } // GetRat - get big.Rat +//func (r Rat) Num() int64 { return r.Rat.Num().Int64() } // Num - return the numerator +//func (r Rat) Denom() int64 { return r.Rat.Denom().Int64() } // Denom - return the denominator +//func (r Rat) IsZero() bool { return r.Num() == 0 } // IsZero - Is the Rat equal to zero +//func (r Rat) Equal(r2 Rat) bool { return r.Rat.Cmp(r2.GetRat()) == 0 } // Equal - rationals are equal +//func (r Rat) GT(r2 Rat) bool { return r.Rat.Cmp(r2.GetRat()) == 1 } // GT - greater than +//func (r Rat) LT(r2 Rat) bool { return r.Rat.Cmp(r2.GetRat()) == -1 } // LT - less than +//func (r Rat) Inv() Rat { return Rat{new(big.Rat).Inv(r.Rat)} } // Inv - inverse +//func (r Rat) Mul(r2 Rat) Rat { return Rat{new(big.Rat).Mul(r.Rat, r2.GetRat())} } // Mul - multiplication +//func (r Rat) Quo(r2 Rat) Rat { return Rat{new(big.Rat).Quo(r.Rat, r2.GetRat())} } // Quo - quotient +//func (r Rat) Add(r2 Rat) Rat { return Rat{new(big.Rat).Add(r.Rat, r2.GetRat())} } // Add - addition +//func (r Rat) Sub(r2 Rat) Rat { return Rat{new(big.Rat).Sub(r.Rat, r2.GetRat())} } // Sub - subtraction +//func (r Rat) String() string { return fmt.Sprintf("%v/%v", r.Num(), r.Denom()) } // Sub - subtraction + +var ( + zero = big.NewInt(0) + one = big.NewInt(1) + two = big.NewInt(2) + five = big.NewInt(5) + nFive = big.NewInt(-5) + ten = big.NewInt(10) +) + +// evaluate the rational using bankers rounding +func (r Rat) EvaluateBig() *big.Int { + + num := r.GetRat().Num() + denom := r.GetRat().Denom() + + d, rem := new(big.Int), new(big.Int) + d.QuoRem(num, denom, rem) + if rem.Cmp(zero) == 0 { // is the remainder zero + return d + } + + // evaluate the remainder using bankers rounding + tenNum := new(big.Int).Mul(num, ten) + tenD := new(big.Int).Mul(d, ten) + remainderDigit := new(big.Int).Sub(new(big.Int).Quo(tenNum, denom), tenD) // get the first remainder digit + isFinalDigit := (new(big.Int).Rem(tenNum, denom).Cmp(zero) == 0) // is this the final digit in the remainder? + + switch { + case isFinalDigit && (remainderDigit.Cmp(five) == 0 || remainderDigit.Cmp(nFive) == 0): + dRem2 := new(big.Int).Rem(d, two) + return new(big.Int).Add(d, dRem2) // always rounds to the even number + case remainderDigit.Cmp(five) != -1: //remainderDigit >= 5: + d.Add(d, one) + case remainderDigit.Cmp(nFive) != 1: //remainderDigit <= -5: + d.Sub(d, one) + } + return d +} + +// evaluate the rational using bankers rounding +func (r Rat) Evaluate() int64 { + return r.EvaluateBig().Int64() +} + +// round Rat with the provided precisionFactor +func (r Rat) Round(precisionFactor int64) Rat { + rTen := ToRat(new(big.Rat).Mul(r.GetRat(), big.NewRat(precisionFactor, 1))) + return ToRat(big.NewRat(rTen.Evaluate(), precisionFactor)) +} + +// TODO panic if negative or if totalDigits < len(initStr)??? +// evaluate as an integer and return left padded string +func (r Rat) ToLeftPadded(totalDigits int8) string { + intStr := r.EvaluateBig().String() + fcode := `%0` + strconv.Itoa(int(totalDigits)) + `s` + return fmt.Sprintf(fcode, intStr) +} + +//___________________________________________________________________________________ + +// Hack to just use json.Marshal for everything until +// we update for amino +//type JSONCodec struct{} +//func (jc JSONCodec) MarshalJSON(o interface{}) ([]byte, error) { return json.Marshal(o) } +//func (jc JSONCodec) UnmarshalJSON(bz []byte, o interface{}) error { return json.Unmarshal(bz, o) } + +// Wraps r.MarshalText() in quotes to make it a valid JSON string. +//func (r Rat) MarshalAmino() (string, error) { +//bz, err := r.MarshalText() +//if err != nil { +//return "", err +//} +//return fmt.Sprintf(`%s`, bz), nil +//} + +//// Requires a valid JSON string - strings quotes and calls UnmarshalText +//func (r *Rat) UnmarshalAmino(data string) (err error) { +////quote := []byte(`"`) +////if len(data) < 2 || +////!bytes.HasPrefix(data, quote) || +////!bytes.HasSuffix(data, quote) { +////return fmt.Errorf("JSON encoded Rat must be a quote-delimitted string") +////} +////data = bytes.Trim(data, `"`) +//return r.UnmarshalText([]byte(data)) +//} diff --git a/types/rational_test.go b/types/rational_test.go new file mode 100644 index 000000000..5af63e8ec --- /dev/null +++ b/types/rational_test.go @@ -0,0 +1,277 @@ +package types + +import ( + "math/big" + "testing" + + wire "github.com/cosmos/cosmos-sdk/wire" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestNew(t *testing.T) { + assert.Equal(t, NewRat(1), NewRat(1, 1)) + assert.Equal(t, NewRat(100), NewRat(100, 1)) + assert.Equal(t, NewRat(-1), NewRat(-1, 1)) + assert.Equal(t, NewRat(-100), NewRat(-100, 1)) + assert.Equal(t, NewRat(0), NewRat(0, 1)) + + // do not allow for more than 2 variables + assert.Panics(t, func() { NewRat(1, 1, 1) }) +} + +func TestNewFromDecimal(t *testing.T) { + tests := []struct { + decimalStr string + expErr bool + exp Rat + }{ + {"0", false, NewRat(0)}, + {"1", false, NewRat(1)}, + {"1.1", false, NewRat(11, 10)}, + {"0.75", false, NewRat(3, 4)}, + {"0.8", false, NewRat(4, 5)}, + {"0.11111", false, NewRat(11111, 100000)}, + {".", true, Rat{}}, + {".0", true, Rat{}}, + {"1.", true, Rat{}}, + {"foobar", true, Rat{}}, + {"0.foobar", true, Rat{}}, + {"0.foobar.", true, Rat{}}, + } + + for _, tc := range tests { + + res, err := NewRatFromDecimal(tc.decimalStr) + if tc.expErr { + assert.NotNil(t, err, tc.decimalStr) + } else { + assert.Nil(t, err) + assert.True(t, res.Equal(tc.exp)) + } + + // negative tc + res, err = NewRatFromDecimal("-" + tc.decimalStr) + if tc.expErr { + assert.NotNil(t, err, tc.decimalStr) + } else { + assert.Nil(t, err) + assert.True(t, res.Equal(tc.exp.Mul(NewRat(-1)))) + } + } +} + +func TestEqualities(t *testing.T) { + tests := []struct { + r1, r2 Rat + gt, lt, eq bool + }{ + {NewRat(0), NewRat(0), false, false, true}, + {NewRat(0, 100), NewRat(0, 10000), false, false, true}, + {NewRat(100), NewRat(100), false, false, true}, + {NewRat(-100), NewRat(-100), false, false, true}, + {NewRat(-100, -1), NewRat(100), false, false, true}, + {NewRat(-1, 1), NewRat(1, -1), false, false, true}, + {NewRat(1, -1), NewRat(-1, 1), false, false, true}, + {NewRat(3, 7), NewRat(3, 7), false, false, true}, + + {NewRat(0), NewRat(3, 7), false, true, false}, + {NewRat(0), NewRat(100), false, true, false}, + {NewRat(-1), NewRat(3, 7), false, true, false}, + {NewRat(-1), NewRat(100), false, true, false}, + {NewRat(1, 7), NewRat(100), false, true, false}, + {NewRat(1, 7), NewRat(3, 7), false, true, false}, + {NewRat(-3, 7), NewRat(-1, 7), false, true, false}, + + {NewRat(3, 7), NewRat(0), true, false, false}, + {NewRat(100), NewRat(0), true, false, false}, + {NewRat(3, 7), NewRat(-1), true, false, false}, + {NewRat(100), NewRat(-1), true, false, false}, + {NewRat(100), NewRat(1, 7), true, false, false}, + {NewRat(3, 7), NewRat(1, 7), true, false, false}, + {NewRat(-1, 7), NewRat(-3, 7), true, false, false}, + } + + for _, tc := range tests { + assert.Equal(t, tc.gt, tc.r1.GT(tc.r2)) + assert.Equal(t, tc.lt, tc.r1.LT(tc.r2)) + assert.Equal(t, tc.eq, tc.r1.Equal(tc.r2)) + } + +} + +func TestArithmatic(t *testing.T) { + tests := []struct { + r1, r2 Rat + resMul, resDiv, resAdd, resSub Rat + }{ + // r1 r2 MUL DIV ADD SUB + {NewRat(0), NewRat(0), NewRat(0), NewRat(0), NewRat(0), NewRat(0)}, + {NewRat(1), NewRat(0), NewRat(0), NewRat(0), NewRat(1), NewRat(1)}, + {NewRat(0), NewRat(1), NewRat(0), NewRat(0), NewRat(1), NewRat(-1)}, + {NewRat(0), NewRat(-1), NewRat(0), NewRat(0), NewRat(-1), NewRat(1)}, + {NewRat(-1), NewRat(0), NewRat(0), NewRat(0), NewRat(-1), NewRat(-1)}, + + {NewRat(1), NewRat(1), NewRat(1), NewRat(1), NewRat(2), NewRat(0)}, + {NewRat(-1), NewRat(-1), NewRat(1), NewRat(1), NewRat(-2), NewRat(0)}, + {NewRat(1), NewRat(-1), NewRat(-1), NewRat(-1), NewRat(0), NewRat(2)}, + {NewRat(-1), NewRat(1), NewRat(-1), NewRat(-1), NewRat(0), NewRat(-2)}, + + {NewRat(3), NewRat(7), NewRat(21), NewRat(3, 7), NewRat(10), NewRat(-4)}, + {NewRat(2), NewRat(4), NewRat(8), NewRat(1, 2), NewRat(6), NewRat(-2)}, + {NewRat(100), NewRat(100), NewRat(10000), NewRat(1), NewRat(200), NewRat(0)}, + + {NewRat(3, 2), NewRat(3, 2), NewRat(9, 4), NewRat(1), NewRat(3), NewRat(0)}, + {NewRat(3, 7), NewRat(7, 3), NewRat(1), NewRat(9, 49), NewRat(58, 21), NewRat(-40, 21)}, + {NewRat(1, 21), NewRat(11, 5), NewRat(11, 105), NewRat(5, 231), NewRat(236, 105), NewRat(-226, 105)}, + {NewRat(-21), NewRat(3, 7), NewRat(-9), NewRat(-49), NewRat(-144, 7), NewRat(-150, 7)}, + {NewRat(100), NewRat(1, 7), NewRat(100, 7), NewRat(700), NewRat(701, 7), NewRat(699, 7)}, + } + + for _, tc := range tests { + assert.True(t, tc.resMul.Equal(tc.r1.Mul(tc.r2)), "r1 %v, r2 %v", tc.r1.GetRat(), tc.r2.GetRat()) + assert.True(t, tc.resAdd.Equal(tc.r1.Add(tc.r2)), "r1 %v, r2 %v", tc.r1.GetRat(), tc.r2.GetRat()) + assert.True(t, tc.resSub.Equal(tc.r1.Sub(tc.r2)), "r1 %v, r2 %v", tc.r1.GetRat(), tc.r2.GetRat()) + + if tc.r2.Num == 0 { // panic for divide by zero + assert.Panics(t, func() { tc.r1.Quo(tc.r2) }) + } else { + assert.True(t, tc.resDiv.Equal(tc.r1.Quo(tc.r2)), "r1 %v, r2 %v", tc.r1.GetRat(), tc.r2.GetRat()) + } + } +} + +func TestEvaluate(t *testing.T) { + tests := []struct { + r1 Rat + res int64 + }{ + {NewRat(0), 0}, + {NewRat(1), 1}, + {NewRat(1, 4), 0}, + {NewRat(1, 2), 0}, + {NewRat(3, 4), 1}, + {NewRat(5, 6), 1}, + {NewRat(3, 2), 2}, + {NewRat(5, 2), 2}, + {NewRat(6, 11), 1}, // 0.545-> 1 even though 5 is first decimal and 1 not even + {NewRat(17, 11), 2}, // 1.545 + {NewRat(5, 11), 0}, + {NewRat(16, 11), 1}, + {NewRat(113, 12), 9}, + } + + for _, tc := range tests { + assert.Equal(t, tc.res, tc.r1.Evaluate(), "%v", tc.r1) + assert.Equal(t, tc.res*-1, tc.r1.Mul(NewRat(-1)).Evaluate(), "%v", tc.r1.Mul(NewRat(-1))) + } +} + +func TestRound(t *testing.T) { + many3 := "333333333333333333333333333333333333333333333" + many7 := "777777777777777777777777777777777777777777777" + big3, worked := new(big.Int).SetString(many3, 10) + require.True(t, worked) + big7, worked := new(big.Int).SetString(many7, 10) + require.True(t, worked) + + tests := []struct { + r, res Rat + precFactor int64 + }{ + {NewRat(333, 777), NewRat(429, 1000), 1000}, + {ToRat(new(big.Rat).SetFrac(big3, big7)), NewRat(429, 1000), 1000}, + {ToRat(new(big.Rat).SetFrac(big3, big7)), ToRat(big.NewRat(4285714286, 10000000000)), 10000000000}, + {NewRat(1, 2), NewRat(1, 2), 1000}, + } + + for _, tc := range tests { + assert.Equal(t, tc.res, tc.r.Round(tc.precFactor), "%v", tc.r) + negR1, negRes := tc.r.Mul(NewRat(-1)), tc.res.Mul(NewRat(-1)) + assert.Equal(t, negRes, negR1.Round(tc.precFactor), "%v", negR1) + } +} + +func TestToLeftPadded(t *testing.T) { + tests := []struct { + rat Rat + digits int8 + res string + }{ + {NewRat(100, 3), 8, "00000033"}, + {NewRat(1, 3), 8, "00000000"}, + {NewRat(100, 2), 8, "00000050"}, + {NewRat(1000, 3), 8, "00000333"}, + {NewRat(1000, 3), 12, "000000000333"}, + } + for _, tc := range tests { + assert.Equal(t, tc.res, tc.rat.ToLeftPadded(tc.digits)) + } +} + +//func TestZeroSerializationJSON(t *testing.T) { +//r := NewRat(0, 1) +//err := r.UnmarshalJSON([]byte(`"0/1"`)) +//assert.Nil(t, err) +//err = r.UnmarshalJSON([]byte(`"0/0"`)) +//assert.NotNil(t, err) +//err = r.UnmarshalJSON([]byte(`"1/0"`)) +//assert.NotNil(t, err) +//err = r.UnmarshalJSON([]byte(`"{}"`)) +//assert.NotNil(t, err) +//} + +//func TestSerializationJSON(t *testing.T) { +//r := NewRat(1, 3) + +//bz, err := r.MarshalText() +//require.Nil(t, err) + +//r2 := NewRat(0, 1) +//err = r2.UnmarshalText(bz) +//require.Nil(t, err) + +//assert.True(t, r.Equal(r2), "original: %v, unmarshalled: %v", r, r2) +//} + +var cdc = wire.NewCodec() //var jsonCdc JSONCodec // TODO wire.Codec + +func TestSerializationGoWire(t *testing.T) { + r := NewRat(1, 3) + + bz, err := cdc.MarshalBinary(r) + require.Nil(t, err) + + //str, err := r.MarshalJSON() + //require.Nil(t, err) + + r2 := NewRat(0, 1) + err = cdc.UnmarshalBinary([]byte(bz), &r2) + //panic(fmt.Sprintf("debug bz: %v\n", string(bz))) + require.Nil(t, err) + + assert.True(t, r.Equal(r2), "original: %v, unmarshalled: %v", r, r2) +} + +type testEmbedStruct struct { + Field1 string `json:"f1"` + Field2 int `json:"f2"` + Field3 Rat `json:"f3"` +} + +func TestEmbeddedStructSerializationGoWire(t *testing.T) { + obj := testEmbedStruct{"foo", 10, NewRat(1, 3)} + + bz, err := cdc.MarshalBinary(obj) + require.Nil(t, err) + + var obj2 testEmbedStruct + obj2.Field3 = NewRat(0, 1) // ... needs to be initialized + err = cdc.UnmarshalBinary(bz, &obj2) + require.Nil(t, err) + + assert.Equal(t, obj.Field1, obj2.Field1) + assert.Equal(t, obj.Field2, obj2.Field2) + assert.True(t, obj.Field3.Equal(obj2.Field3), "original: %v, unmarshalled: %v", obj, obj2) + +} diff --git a/types/tx_msg.go b/types/tx_msg.go index 79272f386..25d35512d 100644 --- a/types/tx_msg.go +++ b/types/tx_msg.go @@ -91,6 +91,7 @@ func NewStdFee(gas int64, amount ...Coin) StdFee { } } +// fee bytes for signing later func (fee StdFee) Bytes() []byte { // normalize. XXX // this is a sign of something ugly @@ -147,6 +148,7 @@ type StdSignMsg struct { // XXX: Alt } +// get message bytes func (msg StdSignMsg) Bytes() []byte { return StdSignBytes(msg.ChainID, msg.Sequences, msg.Fee, msg.Msg) } @@ -171,6 +173,7 @@ func NewTestMsg(addrs ...Address) *TestMsg { } } +//nolint func (msg *TestMsg) Type() string { return "TestMsg" } func (msg *TestMsg) Get(key interface{}) (value interface{}) { return nil } func (msg *TestMsg) GetSignBytes() []byte { diff --git a/version/version.go b/version/version.go index 59f530bfb..28e4bea17 100644 --- a/version/version.go +++ b/version/version.go @@ -6,10 +6,10 @@ package version // TODO improve const Maj = "0" -const Min = "12" +const Min = "13" const Fix = "0" -const Version = "0.12.0" +const Version = "0.13.0" // GitCommit set by build flags var GitCommit = "" diff --git a/x/auth/mapper.go b/x/auth/mapper.go index 1176db3cb..13fe9a844 100644 --- a/x/auth/mapper.go +++ b/x/auth/mapper.go @@ -165,7 +165,6 @@ func (am accountMapper) decodeAccount(bz []byte) sdk.Account { accI := oldwire.ReadBinary(struct{ sdk.Account }{}, r, len(bz), n, err) if *err != nil { panic(*err) - } acc := accI.(struct{ sdk.Account }).Account diff --git a/x/auth/rest/query.go b/x/auth/rest/query.go index 1ef9abe44..22c364ccd 100644 --- a/x/auth/rest/query.go +++ b/x/auth/rest/query.go @@ -16,7 +16,7 @@ import ( type commander struct { storeName string cdc *wire.Codec - decoder sdk.AccountDecoder + decoder sdk.AccountDecoder } func QueryAccountRequestHandler(storeName string, cdc *wire.Codec, decoder sdk.AccountDecoder) func(http.ResponseWriter, *http.Request) { diff --git a/x/bank/commands/sendtx.go b/x/bank/commands/sendtx.go index 5d1a6e05c..5619b4d0f 100644 --- a/x/bank/commands/sendtx.go +++ b/x/bank/commands/sendtx.go @@ -62,19 +62,11 @@ func (c Commander) sendTxCmd(cmd *cobra.Command, args []string) error { // get account name name := viper.GetString(client.FlagName) - // get password - buf := client.BufferStdin() - prompt := fmt.Sprintf("Password to sign with '%s':", name) - passphrase, err := client.GetPassword(prompt, buf) - if err != nil { - return err - } - // build message msg := BuildMsg(from, to, coins) // build and sign the transaction, then broadcast to Tendermint - res, err := builder.SignBuildBroadcast(name, passphrase, msg, c.Cdc) + res, err := builder.SignBuildBroadcast(name, msg, c.Cdc) if err != nil { return err } diff --git a/x/bank/handler.go b/x/bank/handler.go index b5d2a6757..8eca94a86 100644 --- a/x/bank/handler.go +++ b/x/bank/handler.go @@ -6,7 +6,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" ) -// Handle all "bank" type messages. +// NewHandler returns a handler for "bank" type messages. func NewHandler(ck CoinKeeper) sdk.Handler { return func(ctx sdk.Context, msg sdk.Msg) sdk.Result { switch msg := msg.(type) { @@ -25,18 +25,9 @@ func NewHandler(ck CoinKeeper) sdk.Handler { func handleSendMsg(ctx sdk.Context, ck CoinKeeper, msg SendMsg) sdk.Result { // NOTE: totalIn == totalOut should already have been checked - for _, in := range msg.Inputs { - _, err := ck.SubtractCoins(ctx, in.Address, in.Coins) - if err != nil { - return err.Result() - } - } - - for _, out := range msg.Outputs { - _, err := ck.AddCoins(ctx, out.Address, out.Coins) - if err != nil { - return err.Result() - } + err := ck.InputOutputCoins(ctx, msg.Inputs, msg.Outputs) + if err != nil { + return err.Result() } // TODO: add some tags so we can search it! diff --git a/x/bank/mapper.go b/x/bank/keeper.go similarity index 55% rename from x/bank/mapper.go rename to x/bank/keeper.go index 0530967f1..b52b480f6 100644 --- a/x/bank/mapper.go +++ b/x/bank/keeper.go @@ -6,6 +6,8 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" ) +const moduleName = "bank" + // CoinKeeper manages transfers between accounts type CoinKeeper struct { am sdk.AccountMapper @@ -16,6 +18,12 @@ func NewCoinKeeper(am sdk.AccountMapper) CoinKeeper { return CoinKeeper{am: am} } +// GetCoins returns the coins at the addr. +func (ck CoinKeeper) GetCoins(ctx sdk.Context, addr sdk.Address, amt sdk.Coins) sdk.Coins { + acc := ck.am.GetAccount(ctx, addr) + return acc.GetCoins() +} + // SubtractCoins subtracts amt from the coins at the addr. func (ck CoinKeeper) SubtractCoins(ctx sdk.Context, addr sdk.Address, amt sdk.Coins) (sdk.Coins, sdk.Error) { acc := ck.am.GetAccount(ctx, addr) @@ -48,3 +56,37 @@ func (ck CoinKeeper) AddCoins(ctx sdk.Context, addr sdk.Address, amt sdk.Coins) ck.am.SetAccount(ctx, acc) return newCoins, nil } + +// SendCoins moves coins from one account to another +func (ck CoinKeeper) SendCoins(ctx sdk.Context, fromAddr sdk.Address, toAddr sdk.Address, amt sdk.Coins) sdk.Error { + _, err := ck.SubtractCoins(ctx, fromAddr, amt) + if err != nil { + return err + } + + _, err = ck.AddCoins(ctx, toAddr, amt) + if err != nil { + return err + } + + return nil +} + +// InputOutputCoins handles a list of inputs and outputs +func (ck CoinKeeper) InputOutputCoins(ctx sdk.Context, inputs []Input, outputs []Output) sdk.Error { + for _, in := range inputs { + _, err := ck.SubtractCoins(ctx, in.Address, in.Coins) + if err != nil { + return err + } + } + + for _, out := range outputs { + _, err := ck.AddCoins(ctx, out.Address, out.Coins) + if err != nil { + return err + } + } + + return nil +} diff --git a/x/bank/tx.go b/x/bank/msgs.go similarity index 100% rename from x/bank/tx.go rename to x/bank/msgs.go diff --git a/x/bank/tx_test.go b/x/bank/msgs_test.go similarity index 100% rename from x/bank/tx_test.go rename to x/bank/msgs_test.go diff --git a/x/ibc/commands/ibctx.go b/x/ibc/commands/ibctx.go index 17d1e0048..e0186b717 100644 --- a/x/ibc/commands/ibctx.go +++ b/x/ibc/commands/ibctx.go @@ -53,14 +53,8 @@ func (c sendCommander) sendIBCTransfer(cmd *cobra.Command, args []string) error // get password name := viper.GetString(client.FlagName) - buf := client.BufferStdin() - prompt := fmt.Sprintf("Password to sign with '%s':", name) - passphrase, err := client.GetPassword(prompt, buf) - if err != nil { - return err - } - res, err := builder.SignBuildBroadcast(name, passphrase, msg, c.cdc) + res, err := builder.SignBuildBroadcast(name, msg, c.cdc) if err != nil { return err } diff --git a/x/ibc/commands/relay.go b/x/ibc/commands/relay.go index 091b0e920..9f6647ba5 100644 --- a/x/ibc/commands/relay.go +++ b/x/ibc/commands/relay.go @@ -27,7 +27,7 @@ const ( type relayCommander struct { cdc *wire.Codec address sdk.Address - decoder sdk.AccountDecoder + decoder sdk.AccountDecoder mainStore string ibcStore string } @@ -35,7 +35,7 @@ type relayCommander struct { func IBCRelayCmd(cdc *wire.Codec) *cobra.Command { cmdr := relayCommander{ cdc: cdc, - decoder: authcmd.GetAccountDecoder(cdc), + decoder: authcmd.GetAccountDecoder(cdc), ibcStore: "ibc", mainStore: "main", } @@ -80,9 +80,7 @@ func (c relayCommander) runIBCRelay(cmd *cobra.Command, args []string) { func (c relayCommander) loop(fromChainID, fromChainNode, toChainID, toChainNode string) { // get password name := viper.GetString(client.FlagName) - buf := client.BufferStdin() - prompt := fmt.Sprintf("Password to sign with '%s':", name) - passphrase, err := client.GetPassword(prompt, buf) + passphrase, err := builder.GetPassphraseFromStdin(name) if err != nil { panic(err) } diff --git a/x/ibc/ibc_test.go b/x/ibc/ibc_test.go index bec08fb56..1e0431147 100644 --- a/x/ibc/ibc_test.go +++ b/x/ibc/ibc_test.go @@ -15,8 +15,6 @@ import ( "github.com/cosmos/cosmos-sdk/wire" "github.com/cosmos/cosmos-sdk/x/auth" "github.com/cosmos/cosmos-sdk/x/bank" - - "github.com/cosmos/cosmos-sdk/examples/basecoin/x/cool" ) // AccountMapper(/CoinKeeper) and IBCMapper should use different StoreKey later @@ -53,8 +51,6 @@ func makeCodec() *wire.Codec { struct{ sdk.Msg }{}, oldwire.ConcreteType{bank.SendMsg{}, msgTypeSend}, oldwire.ConcreteType{bank.IssueMsg{}, msgTypeIssue}, - oldwire.ConcreteType{cool.QuizMsg{}, msgTypeQuiz}, - oldwire.ConcreteType{cool.SetTrendMsg{}, msgTypeSetTrend}, oldwire.ConcreteType{IBCTransferMsg{}, msgTypeIBCTransferMsg}, oldwire.ConcreteType{IBCReceiveMsg{}, msgTypeIBCReceiveMsg}, ) diff --git a/x/ibc/rest/root.go b/x/ibc/rest/root.go new file mode 100644 index 000000000..81e74d031 --- /dev/null +++ b/x/ibc/rest/root.go @@ -0,0 +1,14 @@ +package rest + +import ( + "github.com/gorilla/mux" + + keys "github.com/tendermint/go-crypto/keys" + + "github.com/cosmos/cosmos-sdk/wire" +) + +// RegisterRoutes - Central function to define routes that get registered by the main application +func RegisterRoutes(r *mux.Router, cdc *wire.Codec, kb keys.Keybase) { + r.HandleFunc("/ibc/{destchain}/{address}/send", TransferRequestHandler(cdc, kb)).Methods("POST") +} diff --git a/x/ibc/rest/transfer.go b/x/ibc/rest/transfer.go new file mode 100644 index 000000000..f47159160 --- /dev/null +++ b/x/ibc/rest/transfer.go @@ -0,0 +1,100 @@ +package rest + +import ( + "encoding/hex" + "encoding/json" + "io/ioutil" + "net/http" + + "github.com/gorilla/mux" + "github.com/spf13/viper" + "github.com/tendermint/go-crypto/keys" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/client/builder" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/wire" + "github.com/cosmos/cosmos-sdk/x/bank/commands" + "github.com/cosmos/cosmos-sdk/x/ibc" +) + +type transferBody struct { + // Fees sdk.Coin `json="fees"` + Amount sdk.Coins `json:"amount"` + LocalAccountName string `json:"name"` + Password string `json:"password"` + SrcChainID string `json:"src_chain_id"` + Sequence int64 `json:"sequence"` +} + +// TransferRequestHandler - http request handler to transfer coins to a address +// on a different chain via IBC +func TransferRequestHandler(cdc *wire.Codec, kb keys.Keybase) func(http.ResponseWriter, *http.Request) { + c := commands.Commander{cdc} + return func(w http.ResponseWriter, r *http.Request) { + // collect data + vars := mux.Vars(r) + destChainID := vars["destchain"] + address := vars["address"] + + var m transferBody + body, err := ioutil.ReadAll(r.Body) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte(err.Error())) + return + } + err = json.Unmarshal(body, &m) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte(err.Error())) + return + } + + info, err := kb.Get(m.LocalAccountName) + if err != nil { + w.WriteHeader(http.StatusUnauthorized) + w.Write([]byte(err.Error())) + return + } + + bz, err := hex.DecodeString(address) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte(err.Error())) + return + } + to := sdk.Address(bz) + + // build message + packet := ibc.NewIBCPacket(info.PubKey.Address(), to, m.Amount, m.SrcChainID, destChainID) + msg := ibc.IBCTransferMsg{packet} + + // sign + // XXX: OMG + viper.Set(client.FlagSequence, m.Sequence) + txBytes, err := builder.SignAndBuild(m.LocalAccountName, m.Password, msg, c.Cdc) + if err != nil { + w.WriteHeader(http.StatusUnauthorized) + w.Write([]byte(err.Error())) + return + } + + // send + res, err := builder.BroadcastTx(txBytes) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(err.Error())) + return + } + + output, err := json.MarshalIndent(res, "", " ") + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(err.Error())) + return + } + + w.Write(output) + } +} diff --git a/x/staking/commands/commands.go b/x/simplestake/commands/commands.go similarity index 84% rename from x/staking/commands/commands.go rename to x/simplestake/commands/commands.go index c2830d395..19d6cddbf 100644 --- a/x/staking/commands/commands.go +++ b/x/simplestake/commands/commands.go @@ -13,7 +13,7 @@ import ( "github.com/cosmos/cosmos-sdk/client/builder" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/wire" - "github.com/cosmos/cosmos-sdk/x/staking" + "github.com/cosmos/cosmos-sdk/x/simplestake" ) const ( @@ -76,7 +76,7 @@ func (co commander) bondTxCmd(cmd *cobra.Command, args []string) error { var pubKeyEd crypto.PubKeyEd25519 copy(pubKeyEd[:], rawPubKey) - msg := staking.NewBondMsg(from, stake, pubKeyEd.Wrap()) + msg := simplestake.NewBondMsg(from, stake, pubKeyEd.Wrap()) return co.sendMsg(msg) } @@ -87,21 +87,14 @@ func (co commander) unbondTxCmd(cmd *cobra.Command, args []string) error { return err } - msg := staking.NewUnbondMsg(from) + msg := simplestake.NewUnbondMsg(from) return co.sendMsg(msg) } func (co commander) sendMsg(msg sdk.Msg) error { name := viper.GetString(client.FlagName) - buf := client.BufferStdin() - prompt := fmt.Sprintf("Password to sign with '%s':", name) - passphrase, err := client.GetPassword(prompt, buf) - if err != nil { - return err - } - - res, err := builder.SignBuildBroadcast(name, passphrase, msg, co.cdc) + res, err := builder.SignBuildBroadcast(name, msg, co.cdc) if err != nil { return err } diff --git a/x/staking/errors.go b/x/simplestake/errors.go similarity index 56% rename from x/staking/errors.go rename to x/simplestake/errors.go index 77f009fe0..f69ffcbeb 100644 --- a/x/staking/errors.go +++ b/x/simplestake/errors.go @@ -1,16 +1,21 @@ -package staking +package simplestake import ( sdk "github.com/cosmos/cosmos-sdk/types" ) const ( - // Staking errors reserve 300 - 399. - CodeEmptyValidator sdk.CodeType = 300 - CodeInvalidUnbond sdk.CodeType = 301 - CodeEmptyStake sdk.CodeType = 302 + // simplestake errors reserve 300 - 399. + CodeEmptyValidator sdk.CodeType = 300 + CodeInvalidUnbond sdk.CodeType = 301 + CodeEmptyStake sdk.CodeType = 302 + CodeIncorrectStakingToken sdk.CodeType = 303 ) +func ErrIncorrectStakingToken() sdk.Error { + return newError(CodeIncorrectStakingToken, "") +} + func ErrEmptyValidator() sdk.Error { return newError(CodeEmptyValidator, "") } diff --git a/x/simplestake/handler.go b/x/simplestake/handler.go new file mode 100644 index 000000000..31e5a7d24 --- /dev/null +++ b/x/simplestake/handler.go @@ -0,0 +1,55 @@ +package simplestake + +import ( + abci "github.com/tendermint/abci/types" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// NewHandler returns a handler for "simplestake" type messages. +func NewHandler(k Keeper) sdk.Handler { + return func(ctx sdk.Context, msg sdk.Msg) sdk.Result { + switch msg := msg.(type) { + case BondMsg: + return handleBondMsg(ctx, k, msg) + case UnbondMsg: + return handleUnbondMsg(ctx, k, msg) + default: + return sdk.ErrUnknownRequest("No match for message type.").Result() + } + } +} + +func handleBondMsg(ctx sdk.Context, k Keeper, msg BondMsg) sdk.Result { + power, err := k.Bond(ctx, msg.Address, msg.PubKey, msg.Stake) + if err != nil { + return err.Result() + } + + valSet := abci.Validator{ + PubKey: msg.PubKey.Bytes(), + Power: power, + } + + return sdk.Result{ + Code: sdk.CodeOK, + ValidatorUpdates: abci.Validators{valSet}, + } +} + +func handleUnbondMsg(ctx sdk.Context, k Keeper, msg UnbondMsg) sdk.Result { + pubKey, _, err := k.Unbond(ctx, msg.Address) + if err != nil { + return err.Result() + } + + valSet := abci.Validator{ + PubKey: pubKey.Bytes(), + Power: int64(0), + } + + return sdk.Result{ + Code: sdk.CodeOK, + ValidatorUpdates: abci.Validators{valSet}, + } +} diff --git a/x/simplestake/keeper.go b/x/simplestake/keeper.go new file mode 100644 index 000000000..e5892f3e2 --- /dev/null +++ b/x/simplestake/keeper.go @@ -0,0 +1,129 @@ +package simplestake + +import ( + crypto "github.com/tendermint/go-crypto" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/wire" + "github.com/cosmos/cosmos-sdk/x/bank" +) + +const stakingToken = "steak" + +const moduleName = "simplestake" + +type Keeper struct { + ck bank.CoinKeeper + + key sdk.StoreKey + cdc *wire.Codec +} + +func NewKeeper(key sdk.StoreKey, coinKeeper bank.CoinKeeper) Keeper { + cdc := wire.NewCodec() + return Keeper{ + key: key, + cdc: cdc, + ck: coinKeeper, + } +} + +func (k Keeper) getBondInfo(ctx sdk.Context, addr sdk.Address) bondInfo { + store := ctx.KVStore(k.key) + bz := store.Get(addr) + if bz == nil { + return bondInfo{} + } + var bi bondInfo + err := k.cdc.UnmarshalBinary(bz, &bi) + if err != nil { + panic(err) + } + return bi +} + +func (k Keeper) setBondInfo(ctx sdk.Context, addr sdk.Address, bi bondInfo) { + store := ctx.KVStore(k.key) + bz, err := k.cdc.MarshalBinary(bi) + if err != nil { + panic(err) + } + store.Set(addr, bz) +} + +func (k Keeper) deleteBondInfo(ctx sdk.Context, addr sdk.Address) { + store := ctx.KVStore(k.key) + store.Delete(addr) +} + +func (k Keeper) Bond(ctx sdk.Context, addr sdk.Address, pubKey crypto.PubKey, stake sdk.Coin) (int64, sdk.Error) { + if stake.Denom != stakingToken { + return 0, ErrIncorrectStakingToken() + } + + _, err := k.ck.SubtractCoins(ctx, addr, []sdk.Coin{stake}) + if err != nil { + return 0, err + } + + bi := k.getBondInfo(ctx, addr) + if bi.isEmpty() { + bi = bondInfo{ + PubKey: pubKey, + Power: 0, + } + } + + bi.Power = bi.Power + stake.Amount + + k.setBondInfo(ctx, addr, bi) + return bi.Power, nil +} + +func (k Keeper) Unbond(ctx sdk.Context, addr sdk.Address) (crypto.PubKey, int64, sdk.Error) { + bi := k.getBondInfo(ctx, addr) + if bi.isEmpty() { + return crypto.PubKey{}, 0, ErrInvalidUnbond() + } + k.deleteBondInfo(ctx, addr) + + returnedBond := sdk.Coin{stakingToken, bi.Power} + + _, err := k.ck.AddCoins(ctx, addr, []sdk.Coin{returnedBond}) + if err != nil { + return bi.PubKey, bi.Power, err + } + + return bi.PubKey, bi.Power, nil +} + +// FOR TESTING PURPOSES ------------------------------------------------- + +func (k Keeper) bondWithoutCoins(ctx sdk.Context, addr sdk.Address, pubKey crypto.PubKey, stake sdk.Coin) (int64, sdk.Error) { + if stake.Denom != stakingToken { + return 0, ErrIncorrectStakingToken() + } + + bi := k.getBondInfo(ctx, addr) + if bi.isEmpty() { + bi = bondInfo{ + PubKey: pubKey, + Power: 0, + } + } + + bi.Power = bi.Power + stake.Amount + + k.setBondInfo(ctx, addr, bi) + return bi.Power, nil +} + +func (k Keeper) unbondWithoutCoins(ctx sdk.Context, addr sdk.Address) (crypto.PubKey, int64, sdk.Error) { + bi := k.getBondInfo(ctx, addr) + if bi.isEmpty() { + return crypto.PubKey{}, 0, ErrInvalidUnbond() + } + k.deleteBondInfo(ctx, addr) + + return bi.PubKey, bi.Power, nil +} diff --git a/x/staking/mapper_test.go b/x/simplestake/keeper_test.go similarity index 53% rename from x/staking/mapper_test.go rename to x/simplestake/keeper_test.go index 8f4e2d50e..9f2615590 100644 --- a/x/staking/mapper_test.go +++ b/x/simplestake/keeper_test.go @@ -1,4 +1,4 @@ -package staking +package simplestake import ( "fmt" @@ -13,25 +13,29 @@ import ( "github.com/cosmos/cosmos-sdk/store" sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/auth" + "github.com/cosmos/cosmos-sdk/x/bank" ) -func setupMultiStore() (sdk.MultiStore, *sdk.KVStoreKey) { +func setupMultiStore() (sdk.MultiStore, *sdk.KVStoreKey, *sdk.KVStoreKey) { db := dbm.NewMemDB() + authKey := sdk.NewKVStoreKey("authkey") capKey := sdk.NewKVStoreKey("capkey") ms := store.NewCommitMultiStore(db) ms.MountStoreWithDB(capKey, sdk.StoreTypeIAVL, db) + ms.MountStoreWithDB(authKey, sdk.StoreTypeIAVL, db) ms.LoadLatestVersion() - return ms, capKey + return ms, authKey, capKey } -func TestStakingMapperGetSet(t *testing.T) { - ms, capKey := setupMultiStore() +func TestKeeperGetSet(t *testing.T) { + ms, _, capKey := setupMultiStore() ctx := sdk.NewContext(ms, abci.Header{}, false, nil) - stakingMapper := NewMapper(capKey) + stakeKeeper := NewKeeper(capKey, bank.NewCoinKeeper(nil)) addr := sdk.Address([]byte("some-address")) - bi := stakingMapper.getBondInfo(ctx, addr) + bi := stakeKeeper.getBondInfo(ctx, addr) assert.Equal(t, bi, bondInfo{}) privKey := crypto.GenPrivKeyEd25519() @@ -41,36 +45,39 @@ func TestStakingMapperGetSet(t *testing.T) { Power: int64(10), } fmt.Printf("Pubkey: %v\n", privKey.PubKey()) - stakingMapper.setBondInfo(ctx, addr, bi) + stakeKeeper.setBondInfo(ctx, addr, bi) - savedBi := stakingMapper.getBondInfo(ctx, addr) + savedBi := stakeKeeper.getBondInfo(ctx, addr) assert.NotNil(t, savedBi) fmt.Printf("Bond Info: %v\n", savedBi) assert.Equal(t, int64(10), savedBi.Power) } func TestBonding(t *testing.T) { - ms, capKey := setupMultiStore() + ms, authKey, capKey := setupMultiStore() ctx := sdk.NewContext(ms, abci.Header{}, false, nil) - stakingMapper := NewMapper(capKey) + + accountMapper := auth.NewAccountMapper(authKey, &auth.BaseAccount{}) + coinKeeper := bank.NewCoinKeeper(accountMapper) + stakeKeeper := NewKeeper(capKey, coinKeeper) addr := sdk.Address([]byte("some-address")) privKey := crypto.GenPrivKeyEd25519() pubKey := privKey.PubKey() - _, _, err := stakingMapper.Unbond(ctx, addr) + _, _, err := stakeKeeper.unbondWithoutCoins(ctx, addr) assert.Equal(t, err, ErrInvalidUnbond()) - _, err = stakingMapper.Bond(ctx, addr, pubKey, 10) + _, err = stakeKeeper.bondWithoutCoins(ctx, addr, pubKey, sdk.Coin{"steak", 10}) assert.Nil(t, err) - power, err := stakingMapper.Bond(ctx, addr, pubKey, 10) + power, err := stakeKeeper.bondWithoutCoins(ctx, addr, pubKey, sdk.Coin{"steak", 10}) assert.Equal(t, int64(20), power) - pk, _, err := stakingMapper.Unbond(ctx, addr) + pk, _, err := stakeKeeper.unbondWithoutCoins(ctx, addr) assert.Nil(t, err) assert.Equal(t, pubKey, pk) - _, _, err = stakingMapper.Unbond(ctx, addr) + _, _, err = stakeKeeper.unbondWithoutCoins(ctx, addr) assert.Equal(t, err, ErrInvalidUnbond()) } diff --git a/x/staking/types.go b/x/simplestake/msgs.go similarity index 96% rename from x/staking/types.go rename to x/simplestake/msgs.go index 9bbf425c7..2ab0e1bad 100644 --- a/x/staking/types.go +++ b/x/simplestake/msgs.go @@ -1,4 +1,4 @@ -package staking +package simplestake import ( "encoding/json" @@ -26,7 +26,7 @@ func NewBondMsg(addr sdk.Address, stake sdk.Coin, pubKey crypto.PubKey) BondMsg } func (msg BondMsg) Type() string { - return "staking" + return moduleName } func (msg BondMsg) ValidateBasic() sdk.Error { @@ -71,7 +71,7 @@ func NewUnbondMsg(addr sdk.Address) UnbondMsg { } func (msg UnbondMsg) Type() string { - return "staking" + return moduleName } func (msg UnbondMsg) ValidateBasic() sdk.Error { diff --git a/x/staking/types_test.go b/x/simplestake/msgs_test.go similarity index 96% rename from x/staking/types_test.go rename to x/simplestake/msgs_test.go index 7c9dd228c..49a8feec5 100644 --- a/x/staking/types_test.go +++ b/x/simplestake/msgs_test.go @@ -1,4 +1,4 @@ -package staking +package simplestake import ( "testing" diff --git a/x/simplestake/types.go b/x/simplestake/types.go new file mode 100644 index 000000000..3371fc977 --- /dev/null +++ b/x/simplestake/types.go @@ -0,0 +1,15 @@ +package simplestake + +import crypto "github.com/tendermint/go-crypto" + +type bondInfo struct { + PubKey crypto.PubKey + Power int64 +} + +func (bi bondInfo) isEmpty() bool { + if bi == (bondInfo{}) { + return true + } + return false +} diff --git a/x/stake/commands/query.go b/x/stake/commands/query.go new file mode 100644 index 000000000..ed436305c --- /dev/null +++ b/x/stake/commands/query.go @@ -0,0 +1,201 @@ +package commands + +import ( + "encoding/hex" + "encoding/json" + "fmt" + + "github.com/spf13/cobra" + flag "github.com/spf13/pflag" + "github.com/spf13/viper" + + crypto "github.com/tendermint/go-crypto" + + "github.com/cosmos/cosmos-sdk/client/builder" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/wire" // XXX fix + "github.com/cosmos/cosmos-sdk/x/stake" +) + +// XXX remove dependancy +func PrefixedKey(app string, key []byte) []byte { + prefix := append([]byte(app), byte(0)) + return append(prefix, key...) +} + +//nolint +var ( + fsValAddr = flag.NewFlagSet("", flag.ContinueOnError) + fsDelAddr = flag.NewFlagSet("", flag.ContinueOnError) + FlagValidatorAddr = "address" + FlagDelegatorAddr = "delegator-address" +) + +func init() { + //Add Flags + fsValAddr.String(FlagValidatorAddr, "", "Address of the validator/candidate") + fsDelAddr.String(FlagDelegatorAddr, "", "Delegator hex address") + +} + +// create command to query for all candidates +func GetCmdQueryCandidates(cdc *wire.Codec, storeName string) *cobra.Command { + cmd := &cobra.Command{ + Use: "candidates", + Short: "Query for the set of validator-candidates pubkeys", + RunE: func(cmd *cobra.Command, args []string) error { + + key := PrefixedKey(stake.MsgType, stake.CandidatesKey) + + res, err := builder.Query(key, storeName) + if err != nil { + return err + } + + // parse out the candidates + candidates := new(stake.Candidates) + err = cdc.UnmarshalJSON(res, candidates) + if err != nil { + return err + } + output, err := json.MarshalIndent(candidates, "", " ") + if err != nil { + return err + } + fmt.Println(string(output)) + return nil + + // TODO output with proofs / machine parseable etc. + }, + } + + cmd.Flags().AddFlagSet(fsDelAddr) + return cmd +} + +// get the command to query a candidate +func GetCmdQueryCandidate(cdc *wire.Codec, storeName string) *cobra.Command { + cmd := &cobra.Command{ + Use: "candidate", + Short: "Query a validator-candidate account", + RunE: func(cmd *cobra.Command, args []string) error { + + addr, err := sdk.GetAddress(viper.GetString(FlagValidatorAddr)) + if err != nil { + return err + } + + key := PrefixedKey(stake.MsgType, stake.GetCandidateKey(addr)) + + res, err := builder.Query(key, storeName) + if err != nil { + return err + } + + // parse out the candidate + candidate := new(stake.Candidate) + err = cdc.UnmarshalBinary(res, candidate) + if err != nil { + return err + } + output, err := json.MarshalIndent(candidate, "", " ") + if err != nil { + return err + } + fmt.Println(string(output)) + return nil + + // TODO output with proofs / machine parseable etc. + }, + } + + cmd.Flags().AddFlagSet(fsValAddr) + return cmd +} + +// get the command to query a single delegator bond +func GetCmdQueryDelegatorBond(cdc *wire.Codec, storeName string) *cobra.Command { + cmd := &cobra.Command{ + Use: "delegator-bond", + Short: "Query a delegators bond based on address and candidate pubkey", + RunE: func(cmd *cobra.Command, args []string) error { + + addr, err := sdk.GetAddress(viper.GetString(FlagValidatorAddr)) + if err != nil { + return err + } + + bz, err := hex.DecodeString(viper.GetString(FlagDelegatorAddr)) + if err != nil { + return err + } + delegator := crypto.Address(bz) + + key := PrefixedKey(stake.MsgType, stake.GetDelegatorBondKey(delegator, addr, cdc)) + + res, err := builder.Query(key, storeName) + if err != nil { + return err + } + + // parse out the bond + var bond stake.DelegatorBond + err = cdc.UnmarshalBinary(res, bond) + if err != nil { + return err + } + output, err := json.MarshalIndent(bond, "", " ") + if err != nil { + return err + } + fmt.Println(string(output)) + return nil + + // TODO output with proofs / machine parseable etc. + }, + } + + cmd.Flags().AddFlagSet(fsValAddr) + cmd.Flags().AddFlagSet(fsDelAddr) + return cmd +} + +// get the command to query all the candidates bonded to a delegator +func GetCmdQueryDelegatorBonds(cdc *wire.Codec, storeName string) *cobra.Command { + cmd := &cobra.Command{ + Use: "delegator-candidates", + Short: "Query all delegators candidates' pubkeys based on address", + RunE: func(cmd *cobra.Command, args []string) error { + + bz, err := hex.DecodeString(viper.GetString(FlagDelegatorAddr)) + if err != nil { + return err + } + delegator := crypto.Address(bz) + + key := PrefixedKey(stake.MsgType, stake.GetDelegatorBondsKey(delegator, cdc)) + + res, err := builder.Query(key, storeName) + if err != nil { + return err + } + + // parse out the candidates list + var candidates []crypto.PubKey + err = cdc.UnmarshalBinary(res, candidates) + if err != nil { + return err + } + output, err := json.MarshalIndent(candidates, "", " ") + if err != nil { + return err + } + fmt.Println(string(output)) + return nil + + // TODO output with proofs / machine parseable etc. + }, + } + cmd.Flags().AddFlagSet(fsDelAddr) + return cmd +} diff --git a/x/stake/commands/tx.go b/x/stake/commands/tx.go new file mode 100644 index 000000000..90b289de7 --- /dev/null +++ b/x/stake/commands/tx.go @@ -0,0 +1,255 @@ +package commands + +import ( + "encoding/hex" + "fmt" + + "github.com/spf13/cobra" + flag "github.com/spf13/pflag" + "github.com/spf13/viper" + + crypto "github.com/tendermint/go-crypto" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/client/builder" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/wire" + "github.com/cosmos/cosmos-sdk/x/stake" +) + +// nolint +const ( + FlagAddressDelegator = "addressD" + FlagAddressCandidate = "addressC" + FlagPubKey = "pubkey" + FlagAmount = "amount" + FlagShares = "shares" + + FlagMoniker = "moniker" + FlagIdentity = "keybase-sig" + FlagWebsite = "website" + FlagDetails = "details" +) + +// common flagsets to add to various functions +var ( + fsPk = flag.NewFlagSet("", flag.ContinueOnError) + fsAmount = flag.NewFlagSet("", flag.ContinueOnError) + fsShares = flag.NewFlagSet("", flag.ContinueOnError) + fsCandidate = flag.NewFlagSet("", flag.ContinueOnError) + fsDelegator = flag.NewFlagSet("", flag.ContinueOnError) +) + +func init() { + fsPk.String(FlagPubKey, "", "PubKey of the validator-candidate") + fsAmount.String(FlagAmount, "1fermion", "Amount of coins to bond") + fsShares.String(FlagShares, "", "Amount of shares to unbond, either in decimal or keyword MAX (ex. 1.23456789, 99, MAX)") + fsCandidate.String(FlagMoniker, "", "validator-candidate name") + fsCandidate.String(FlagIdentity, "", "optional keybase signature") + fsCandidate.String(FlagWebsite, "", "optional website") + fsCandidate.String(FlagAddressCandidate, "", "hex address of the validator/candidate") + fsDelegator.String(FlagAddressDelegator, "", "hex address of the delegator") +} + +//TODO refactor to common functionality +func getNamePassword() (name, passphrase string, err error) { + name = viper.GetString(client.FlagName) + buf := client.BufferStdin() + prompt := fmt.Sprintf("Password to sign with '%s':", name) + passphrase, err = client.GetPassword(prompt, buf) + return +} + +//_________________________________________________________________________________________ + +// create declare candidacy command +func GetCmdDeclareCandidacy(cdc *wire.Codec) *cobra.Command { + cmd := &cobra.Command{ + Use: "declare-candidacy", + Short: "create new validator-candidate account and delegate some coins to it", + RunE: func(cmd *cobra.Command, args []string) error { + amount, err := sdk.ParseCoin(viper.GetString(FlagAmount)) + if err != nil { + return err + } + candidateAddr, err := sdk.GetAddress(viper.GetString(FlagAddressCandidate)) + if err != nil { + return err + } + pk, err := GetPubKey(viper.GetString(FlagPubKey)) + if err != nil { + return err + } + if viper.GetString(FlagMoniker) == "" { + return fmt.Errorf("please enter a moniker for the validator-candidate using --moniker") + } + description := stake.Description{ + Moniker: viper.GetString(FlagMoniker), + Identity: viper.GetString(FlagIdentity), + Website: viper.GetString(FlagWebsite), + Details: viper.GetString(FlagDetails), + } + msg := stake.NewMsgDeclareCandidacy(candidateAddr, pk, amount, description) + + // build and sign the transaction, then broadcast to Tendermint + name := viper.GetString(client.FlagName) + res, err := builder.SignBuildBroadcast(name, 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().AddFlagSet(fsPk) + cmd.Flags().AddFlagSet(fsAmount) + cmd.Flags().AddFlagSet(fsCandidate) + return cmd +} + +// create edit candidacy command +func GetCmdEditCandidacy(cdc *wire.Codec) *cobra.Command { + cmd := &cobra.Command{ + Use: "edit-candidacy", + Short: "edit and existing validator-candidate account", + RunE: func(cmd *cobra.Command, args []string) error { + + candidateAddr, err := sdk.GetAddress(viper.GetString(FlagAddressCandidate)) + if err != nil { + return err + } + description := stake.Description{ + Moniker: viper.GetString(FlagMoniker), + Identity: viper.GetString(FlagIdentity), + Website: viper.GetString(FlagWebsite), + Details: viper.GetString(FlagDetails), + } + msg := stake.NewMsgEditCandidacy(candidateAddr, description) + + // build and sign the transaction, then broadcast to Tendermint + name := viper.GetString(client.FlagName) + res, err := builder.SignBuildBroadcast(name, 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().AddFlagSet(fsPk) + cmd.Flags().AddFlagSet(fsCandidate) + return cmd +} + +// create edit candidacy command +func GetCmdDelegate(cdc *wire.Codec) *cobra.Command { + cmd := &cobra.Command{ + Use: "delegate", + Short: "delegate coins to an existing validator/candidate", + RunE: func(cmd *cobra.Command, args []string) error { + amount, err := sdk.ParseCoin(viper.GetString(FlagAmount)) + if err != nil { + return err + } + + delegatorAddr, err := sdk.GetAddress(viper.GetString(FlagAddressDelegator)) + candidateAddr, err := sdk.GetAddress(viper.GetString(FlagAddressCandidate)) + if err != nil { + return err + } + + msg := stake.NewMsgDelegate(delegatorAddr, candidateAddr, amount) + + // build and sign the transaction, then broadcast to Tendermint + name := viper.GetString(client.FlagName) + res, err := builder.SignBuildBroadcast(name, 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().AddFlagSet(fsPk) + cmd.Flags().AddFlagSet(fsAmount) + cmd.Flags().AddFlagSet(fsDelegator) + return cmd +} + +// create edit candidacy command +func GetCmdUnbond(cdc *wire.Codec) *cobra.Command { + cmd := &cobra.Command{ + Use: "unbond", + Short: "unbond coins from a validator/candidate", + RunE: func(cmd *cobra.Command, args []string) error { + + // check the shares before broadcasting + sharesStr := viper.GetString(FlagShares) + var shares sdk.Rat + if sharesStr != "MAX" { + var err error + shares, err = sdk.NewRatFromDecimal(sharesStr) + if err != nil { + return err + } + if !shares.GT(sdk.ZeroRat) { + return fmt.Errorf("shares must be positive integer or decimal (ex. 123, 1.23456789)") + } + } + + delegatorAddr, err := sdk.GetAddress(viper.GetString(FlagAddressDelegator)) + candidateAddr, err := sdk.GetAddress(viper.GetString(FlagAddressCandidate)) + if err != nil { + return err + } + + msg := stake.NewMsgUnbond(delegatorAddr, candidateAddr, sharesStr) + + // build and sign the transaction, then broadcast to Tendermint + name := viper.GetString(client.FlagName) + res, err := builder.SignBuildBroadcast(name, 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().AddFlagSet(fsPk) + cmd.Flags().AddFlagSet(fsShares) + cmd.Flags().AddFlagSet(fsDelegator) + return cmd +} + +//______________________________________________________________________________________ + +// create the pubkey from a pubkey string +// TODO move to a better reusable place +func GetPubKey(pubKeyStr string) (pk crypto.PubKey, err error) { + + if len(pubKeyStr) == 0 { + err = fmt.Errorf("must use --pubkey flag") + return + } + if len(pubKeyStr) != 64 { //if len(pkBytes) != 32 { + err = fmt.Errorf("pubkey must be Ed25519 hex encoded string which is 64 characters long") + return + } + var pkBytes []byte + pkBytes, err = hex.DecodeString(pubKeyStr) + if err != nil { + return + } + var pkEd crypto.PubKeyEd25519 + copy(pkEd[:], pkBytes[:]) + pk = pkEd.Wrap() + return +} diff --git a/x/stake/errors.go b/x/stake/errors.go new file mode 100644 index 000000000..bd1992959 --- /dev/null +++ b/x/stake/errors.go @@ -0,0 +1,117 @@ +// nolint +package stake + +import ( + "fmt" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +type CodeType = sdk.CodeType + +const ( + // Gaia errors reserve 200 ~ 299. + CodeInvalidValidator CodeType = 201 + CodeInvalidCandidate CodeType = 202 + CodeInvalidBond CodeType = 203 + CodeInvalidInput CodeType = 204 + CodeUnauthorized CodeType = sdk.CodeUnauthorized + CodeInternal CodeType = sdk.CodeInternal + CodeUnknownRequest CodeType = sdk.CodeUnknownRequest +) + +// NOTE: Don't stringer this, we'll put better messages in later. +func codeToDefaultMsg(code CodeType) string { + switch code { + case CodeInvalidValidator: + return "Invalid Validator" + case CodeInvalidCandidate: + return "Invalid Candidate" + case CodeInvalidBond: + return "Invalid Bond" + case CodeInvalidInput: + return "Invalid Input" + case CodeUnauthorized: + return "Unauthorized" + case CodeInternal: + return "Internal Error" + case CodeUnknownRequest: + return "Unknown request" + default: + return sdk.CodeToDefaultMsg(code) + } +} + +//---------------------------------------- +// Error constructors + +func ErrNotEnoughBondShares(shares string) sdk.Error { + return newError(CodeInvalidBond, fmt.Sprintf("not enough shares only have %v", shares)) +} +func ErrCandidateEmpty() sdk.Error { + return newError(CodeInvalidValidator, "Cannot bond to an empty candidate") +} +func ErrBadBondingDenom() sdk.Error { + return newError(CodeInvalidBond, "Invalid coin denomination") +} +func ErrBadBondingAmount() sdk.Error { + return newError(CodeInvalidBond, "Amount must be > 0") +} +func ErrNoBondingAcct() sdk.Error { + return newError(CodeInvalidValidator, "No bond account for this (address, validator) pair") +} +func ErrCommissionNegative() sdk.Error { + return newError(CodeInvalidValidator, "Commission must be positive") +} +func ErrCommissionHuge() sdk.Error { + return newError(CodeInvalidValidator, "Commission cannot be more than 100%") +} +func ErrBadValidatorAddr() sdk.Error { + return newError(CodeInvalidValidator, "Validator does not exist for that address") +} +func ErrBadCandidateAddr() sdk.Error { + return newError(CodeInvalidValidator, "Candidate does not exist for that address") +} +func ErrBadDelegatorAddr() sdk.Error { + return newError(CodeInvalidValidator, "Delegator does not exist for that address") +} +func ErrCandidateExistsAddr() sdk.Error { + return newError(CodeInvalidValidator, "Candidate already exist, cannot re-declare candidacy") +} +func ErrMissingSignature() sdk.Error { + return newError(CodeInvalidValidator, "Missing signature") +} +func ErrBondNotNominated() sdk.Error { + return newError(CodeInvalidValidator, "Cannot bond to non-nominated account") +} +func ErrNoCandidateForAddress() sdk.Error { + return newError(CodeInvalidValidator, "Validator does not exist for that address") +} +func ErrNoDelegatorForAddress() sdk.Error { + return newError(CodeInvalidValidator, "Delegator does not contain validator bond") +} +func ErrInsufficientFunds() sdk.Error { + return newError(CodeInvalidInput, "Insufficient bond shares") +} +func ErrBadShares() sdk.Error { + return newError(CodeInvalidInput, "bad shares provided as input, must be MAX or decimal") +} +func ErrBadRemoveValidator() sdk.Error { + return newError(CodeInvalidValidator, "Error removing validator") +} + +//---------------------------------------- + +// TODO group with code from x/bank/errors.go + +func msgOrDefaultMsg(msg string, code CodeType) string { + if msg != "" { + return msg + } + return codeToDefaultMsg(code) +} + +func newError(code CodeType, msg string) sdk.Error { + msg = msgOrDefaultMsg(msg, code) + return sdk.NewError(code, msg) +} diff --git a/x/stake/handler.go b/x/stake/handler.go new file mode 100644 index 000000000..7449141aa --- /dev/null +++ b/x/stake/handler.go @@ -0,0 +1,304 @@ +package stake + +import ( + "bytes" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/bank" +) + +//nolint +const ( + GasDeclareCandidacy int64 = 20 + GasEditCandidacy int64 = 20 + GasDelegate int64 = 20 + GasUnbond int64 = 20 +) + +//XXX fix initstater +// separated for testing +//func InitState(ctx sdk.Context, k Keeper, key, value string) sdk.Error { + +//params := k.GetParams(ctx) +//switch key { +//case "allowed_bond_denom": +//params.BondDenom = value +//case "max_vals", "gas_bond", "gas_unbond": + +//i, err := strconv.Atoi(value) +//if err != nil { +//return sdk.ErrUnknownRequest(fmt.Sprintf("input must be integer, Error: %v", err.Error())) +//} + +//switch key { +//case "max_vals": +//if i < 0 { +//return sdk.ErrUnknownRequest("cannot designate negative max validators") +//} +//params.MaxValidators = uint16(i) +//case "gas_bond": +//GasDelegate = int64(i) +//case "gas_unbound": +//GasUnbond = int64(i) +//} +//default: +//return sdk.ErrUnknownRequest(key) +//} + +//k.setParams(params) +//return nil +//} + +//_______________________________________________________________________ + +func NewHandler(k Keeper, ck bank.CoinKeeper) sdk.Handler { + return func(ctx sdk.Context, msg sdk.Msg) sdk.Result { + // NOTE msg already has validate basic run + switch msg := msg.(type) { + case MsgDeclareCandidacy: + return handleMsgDeclareCandidacy(ctx, msg, k) + case MsgEditCandidacy: + return handleMsgEditCandidacy(ctx, msg, k) + case MsgDelegate: + return handleMsgDelegate(ctx, msg, k) + case MsgUnbond: + return handleMsgUnbond(ctx, msg, k) + default: + return sdk.ErrTxDecode("invalid message parse in staking module").Result() + } + } +} + +//_____________________________________________________________________ + +// XXX should be send in the msg (init in CLI) +//func getSender() sdk.Address { +//signers := msg.GetSigners() +//if len(signers) != 1 { +//return sdk.ErrUnauthorized("there can only be one signer for staking transaction").Result() +//} +//sender := signers[0] +//} + +//_____________________________________________________________________ + +// These functions assume everything has been authenticated, +// now we just perform action and save + +func handleMsgDeclareCandidacy(ctx sdk.Context, msg MsgDeclareCandidacy, k Keeper) sdk.Result { + + // check to see if the pubkey or sender has been registered before + _, found := k.GetCandidate(ctx, msg.CandidateAddr) + if found { + return ErrCandidateExistsAddr().Result() + } + if msg.Bond.Denom != k.GetParams(ctx).BondDenom { + return ErrBadBondingDenom().Result() + } + if ctx.IsCheckTx() { + return sdk.Result{ + GasUsed: GasDeclareCandidacy, + } + } + + candidate := NewCandidate(msg.CandidateAddr, msg.PubKey, msg.Description) + k.setCandidate(ctx, candidate) + + // move coins from the msg.Address account to a (self-bond) delegator account + // the candidate account and global shares are updated within here + return delegateWithCandidate(ctx, k, msg.CandidateAddr, msg.Bond, candidate).Result() +} + +func handleMsgEditCandidacy(ctx sdk.Context, msg MsgEditCandidacy, k Keeper) sdk.Result { + + // candidate must already be registered + candidate, found := k.GetCandidate(ctx, msg.CandidateAddr) + if !found { + return ErrBadCandidateAddr().Result() + } + if ctx.IsCheckTx() { + return sdk.Result{ + GasUsed: GasEditCandidacy, + } + } + if candidate.Status == Unbonded { //candidate has been withdrawn + return ErrBondNotNominated().Result() + } + + // XXX move to types + // replace all editable fields (clients should autofill existing values) + candidate.Description.Moniker = msg.Description.Moniker + candidate.Description.Identity = msg.Description.Identity + candidate.Description.Website = msg.Description.Website + candidate.Description.Details = msg.Description.Details + + k.setCandidate(ctx, candidate) + return sdk.Result{} +} + +func handleMsgDelegate(ctx sdk.Context, msg MsgDelegate, k Keeper) sdk.Result { + + candidate, found := k.GetCandidate(ctx, msg.CandidateAddr) + if !found { + return ErrBadCandidateAddr().Result() + } + if msg.Bond.Denom != k.GetParams(ctx).BondDenom { + return ErrBadBondingDenom().Result() + } + if ctx.IsCheckTx() { + return sdk.Result{ + GasUsed: GasDelegate, + } + } + return delegateWithCandidate(ctx, k, msg.DelegatorAddr, msg.Bond, candidate).Result() +} + +func delegateWithCandidate(ctx sdk.Context, k Keeper, delegatorAddr sdk.Address, + bondAmt sdk.Coin, candidate Candidate) sdk.Error { + + if candidate.Status == Revoked { //candidate has been withdrawn + return ErrBondNotNominated() + } + + // Get or create the delegator bond + existingBond, found := k.getDelegatorBond(ctx, delegatorAddr, candidate.Address) + if !found { + existingBond = DelegatorBond{ + DelegatorAddr: delegatorAddr, + CandidateAddr: candidate.Address, + Shares: sdk.ZeroRat, + } + } + + // Account new shares, save + err := BondCoins(ctx, k, existingBond, candidate, bondAmt) + if err != nil { + return err + } + k.setDelegatorBond(ctx, existingBond) + k.setCandidate(ctx, candidate) + return nil +} + +// Perform all the actions required to bond tokens to a delegator bond from their account +func BondCoins(ctx sdk.Context, k Keeper, bond DelegatorBond, candidate Candidate, amount sdk.Coin) sdk.Error { + + _, err := k.coinKeeper.SubtractCoins(ctx, bond.DelegatorAddr, sdk.Coins{amount}) + if err != nil { + return err + } + newShares := k.candidateAddTokens(ctx, candidate, amount.Amount) + bond.Shares = bond.Shares.Add(newShares) + k.setDelegatorBond(ctx, bond) + return nil +} + +func handleMsgUnbond(ctx sdk.Context, msg MsgUnbond, k Keeper) sdk.Result { + + // check if bond has any shares in it unbond + bond, found := k.getDelegatorBond(ctx, msg.DelegatorAddr, msg.CandidateAddr) + if !found { + return ErrNoDelegatorForAddress().Result() + } + if !bond.Shares.GT(sdk.ZeroRat) { // bond shares < msg shares + return ErrInsufficientFunds().Result() + } + + // test getting rational number from decimal provided + shares, err := sdk.NewRatFromDecimal(msg.Shares) + if err != nil { + return err.Result() + } + + // test that there are enough shares to unbond + if msg.Shares == "MAX" { + if !bond.Shares.GT(sdk.ZeroRat) { + return ErrNotEnoughBondShares(msg.Shares).Result() + } + } else { + if !bond.Shares.GT(shares) { + return ErrNotEnoughBondShares(msg.Shares).Result() + } + } + + // get candidate + candidate, found := k.GetCandidate(ctx, msg.CandidateAddr) + if !found { + return ErrNoCandidateForAddress().Result() + } + + if ctx.IsCheckTx() { + return sdk.Result{ + GasUsed: GasUnbond, + } + } + + // retrieve the amount of bonds to remove (TODO remove redundancy already serialized) + if msg.Shares == "MAX" { + shares = bond.Shares + } + + // subtract bond tokens from delegator bond + bond.Shares = bond.Shares.Sub(shares) + + // remove the bond + revokeCandidacy := false + if bond.Shares.IsZero() { + + // if the bond is the owner of the candidate then + // trigger a revoke candidacy + if bytes.Equal(bond.DelegatorAddr, candidate.Address) && + candidate.Status != Revoked { + revokeCandidacy = true + } + + k.removeDelegatorBond(ctx, bond) + } else { + k.setDelegatorBond(ctx, bond) + } + + // Add the coins + returnAmount := k.candidateRemoveShares(ctx, candidate, shares) + returnCoins := sdk.Coins{{k.GetParams(ctx).BondDenom, returnAmount}} + k.coinKeeper.AddCoins(ctx, bond.DelegatorAddr, returnCoins) + + // revoke candidate if necessary + if revokeCandidacy { + + // change the share types to unbonded if they were not already + if candidate.Status == Bonded { + k.bondedToUnbondedPool(ctx, candidate) + } + + // lastly update the status + candidate.Status = Revoked + } + + // deduct shares from the candidate + if candidate.Liabilities.IsZero() { + k.removeCandidate(ctx, candidate.Address) + } else { + k.setCandidate(ctx, candidate) + } + return sdk.Result{} +} + +// XXX where this used +// Perform all the actions required to bond tokens to a delegator bond from their account +func UnbondCoins(ctx sdk.Context, k Keeper, bond DelegatorBond, candidate Candidate, shares sdk.Rat) sdk.Error { + + // subtract bond tokens from delegator bond + if bond.Shares.LT(shares) { + return sdk.ErrInsufficientFunds("") //XXX variables inside + } + bond.Shares = bond.Shares.Sub(shares) + + returnAmount := k.candidateRemoveShares(ctx, candidate, shares) + returnCoins := sdk.Coins{{k.GetParams(ctx).BondDenom, returnAmount}} + + _, err := k.coinKeeper.AddCoins(ctx, candidate.Address, returnCoins) + if err != nil { + return err + } + return nil +} diff --git a/x/stake/handler_test.go b/x/stake/handler_test.go new file mode 100644 index 000000000..b6953df5c --- /dev/null +++ b/x/stake/handler_test.go @@ -0,0 +1,248 @@ +package stake + +//import ( +//"strconv" +//"testing" + +//"github.com/stretchr/testify/assert" +//"github.com/stretchr/testify/require" + +//crypto "github.com/tendermint/go-crypto" + +//sdk "github.com/cosmos/cosmos-sdk/types" +//) + +////______________________________________________________________________ + +//func newTestMsgDeclareCandidacy(address sdk.Address, pubKey crypto.PubKey, amt int64) MsgDeclareCandidacy { +//return MsgDeclareCandidacy{ +//Description: Description{}, +//CandidateAddr: address, +//Bond: sdk.Coin{"fermion", amt}, +//PubKey: pubKey, +//} +//} + +//func newTestMsgDelegate(amt int64, delegatorAddr, candidateAddr sdk.Address) MsgDelegate { +//return MsgDelegate{ +//DelegatorAddr: delegatorAddr, +//CandidateAddr: candidateAddr, +//Bond: sdk.Coin{"fermion", amt}, +//} +//} + +//func TestDuplicatesMsgDeclareCandidacy(t *testing.T) { +//ctxDeliver, _, keeper := createTestInput(t, addrs[0], false, 1000) +//ctxCheck, _, keeper := createTestInput(t, addrs[0], true, 1000) + +//msgDeclareCandidacy := newTestMsgDeclareCandidacy(addrs[0], pks[0], 10) +//got := deliverer.declareCandidacy(msgDeclareCandidacy) +//assert.NoError(t, got, "expected no error on runMsgDeclareCandidacy") + +//// one sender can bond to two different addresses +//msgDeclareCandidacy.Address = addrs[1] +//err := checker.declareCandidacy(msgDeclareCandidacy) +//assert.Nil(t, err, "didn't expected error on checkTx") + +//// two addrs cant bond to the same pubkey +//checker.sender = addrs[1] +//msgDeclareCandidacy.Address = addrs[0] +//err = checker.declareCandidacy(msgDeclareCandidacy) +//assert.NotNil(t, err, "expected error on checkTx") +//} + +//func TestIncrementsMsgDelegate(t *testing.T) { +//_, _, mapper, deliverer := createTestInput(t, addrs[0], false, 1000) + +//// first declare candidacy +//bondAmount := int64(10) +//msgDeclareCandidacy := newTestMsgDeclareCandidacy(addrs[0], pks[0], bondAmount) +//got := deliverer.declareCandidacy(msgDeclareCandidacy) +//assert.NoError(t, got, "expected declare candidacy msg to be ok, got %v", got) +//expectedBond := bondAmount // 1 since we send 1 at the start of loop, + +//// just send the same msgbond multiple times +//msgDelegate := newTestMsgDelegate(bondAmount, addrs[0]) +//for i := 0; i < 5; i++ { +//got := deliverer.delegate(msgDelegate) +//assert.NoError(t, got, "expected msg %d to be ok, got %v", i, got) + +////Check that the accounts and the bond account have the appropriate values +//candidates := mapper.GetCandidates() +//expectedBond += bondAmount +////expectedSender := initSender - expectedBond +//gotBonded := candidates[0].Liabilities.Evaluate() +////gotSender := accStore[string(deliverer.sender)] //XXX use StoreMapper +//assert.Equal(t, expectedBond, gotBonded, "i: %v, %v, %v", i, expectedBond, gotBonded) +////assert.Equal(t, expectedSender, gotSender, "i: %v, %v, %v", i, expectedSender, gotSender) // XXX fix +//} +//} + +//func TestIncrementsMsgUnbond(t *testing.T) { +//_, _, mapper, deliverer := createTestInput(t, addrs[0], false, 0) + +//// set initial bond +//initBond := int64(1000) +////accStore[string(deliverer.sender)] = initBond //XXX use StoreMapper +//got := deliverer.declareCandidacy(newTestMsgDeclareCandidacy(addrs[0], pks[0], initBond)) +//assert.NoError(t, got, "expected initial bond msg to be ok, got %v", got) + +//// just send the same msgunbond multiple times +//// XXX use decimals here +//unbondShares, unbondSharesStr := int64(10), "10" +//msgUndelegate := NewMsgUnbond(addrs[0], unbondSharesStr) +//nUnbonds := 5 +//for i := 0; i < nUnbonds; i++ { +//got := deliverer.unbond(msgUndelegate) +//assert.NoError(t, got, "expected msg %d to be ok, got %v", i, got) + +////Check that the accounts and the bond account have the appropriate values +//candidates := mapper.GetCandidates() +//expectedBond := initBond - int64(i+1)*unbondShares // +1 since we send 1 at the start of loop +////expectedSender := initSender + (initBond - expectedBond) +//gotBonded := candidates[0].Liabilities.Evaluate() +////gotSender := accStore[string(deliverer.sender)] // XXX use storemapper + +//assert.Equal(t, expectedBond, gotBonded, "%v, %v", expectedBond, gotBonded) +////assert.Equal(t, expectedSender, gotSender, "%v, %v", expectedSender, gotSender) //XXX fix +//} + +//// these are more than we have bonded now +//errorCases := []int64{ +////1<<64 - 1, // more than int64 +////1<<63 + 1, // more than int64 +//1<<63 - 1, +//1 << 31, +//initBond, +//} +//for _, c := range errorCases { +//unbondShares := strconv.Itoa(int(c)) +//msgUndelegate := NewMsgUnbond(addrs[0], unbondShares) +//got = deliverer.unbond(msgUndelegate) +//assert.Error(t, got, "expected unbond msg to fail") +//} + +//leftBonded := initBond - unbondShares*int64(nUnbonds) + +//// should be unable to unbond one more than we have +//msgUndelegate = NewMsgUnbond(addrs[0], strconv.Itoa(int(leftBonded)+1)) +//got = deliverer.unbond(msgUndelegate) +//assert.Error(t, got, "expected unbond msg to fail") + +//// should be able to unbond just what we have +//msgUndelegate = NewMsgUnbond(addrs[0], strconv.Itoa(int(leftBonded))) +//got = deliverer.unbond(msgUndelegate) +//assert.NoError(t, got, "expected unbond msg to pass") +//} + +//func TestMultipleMsgDeclareCandidacy(t *testing.T) { +//initSender := int64(1000) +//ctx, accStore, mapper, deliverer := createTestInput(t, addrs[0], false, initSender) +//addrs := []sdk.Address{addrs[0], addrs[1], addrs[2]} + +//// bond them all +//for i, addr := range addrs { +//msgDeclareCandidacy := newTestMsgDeclareCandidacy(addrs[i], pks[i], 10) +//deliverer.sender = addr +//got := deliverer.declareCandidacy(msgDeclareCandidacy) +//assert.NoError(t, got, "expected msg %d to be ok, got %v", i, got) + +////Check that the account is bonded +//candidates := mapper.GetCandidates() +//require.Equal(t, i, len(candidates)) +//val := candidates[i] +//balanceExpd := initSender - 10 +//balanceGot := accStore.GetAccount(ctx, val.Address).GetCoins() +//assert.Equal(t, i+1, len(candidates), "expected %d candidates got %d, candidates: %v", i+1, len(candidates), candidates) +//assert.Equal(t, 10, int(val.Liabilities.Evaluate()), "expected %d shares, got %d", 10, val.Liabilities) +//assert.Equal(t, balanceExpd, balanceGot, "expected account to have %d, got %d", balanceExpd, balanceGot) +//} + +//// unbond them all +//for i, addr := range addrs { +//candidatePre := mapper.GetCandidate(addrs[i]) +//msgUndelegate := NewMsgUnbond(addrs[i], "10") +//deliverer.sender = addr +//got := deliverer.unbond(msgUndelegate) +//assert.NoError(t, got, "expected msg %d to be ok, got %v", i, got) + +////Check that the account is unbonded +//candidates := mapper.GetCandidates() +//assert.Equal(t, len(addrs)-(i+1), len(candidates), "expected %d candidates got %d", len(addrs)-(i+1), len(candidates)) + +//candidatePost := mapper.GetCandidate(addrs[i]) +//balanceExpd := initSender +//balanceGot := accStore.GetAccount(ctx, candidatePre.Address).GetCoins() +//assert.Nil(t, candidatePost, "expected nil candidate retrieve, got %d", 0, candidatePost) +//assert.Equal(t, balanceExpd, balanceGot, "expected account to have %d, got %d", balanceExpd, balanceGot) +//} +//} + +//func TestMultipleMsgDelegate(t *testing.T) { +//sender, delegators := addrs[0], addrs[1:] +//_, _, mapper, deliverer := createTestInput(t, addrs[0], false, 1000) + +////first make a candidate +//msgDeclareCandidacy := newTestMsgDeclareCandidacy(sender, pks[0], 10) +//got := deliverer.declareCandidacy(msgDeclareCandidacy) +//require.NoError(t, got, "expected msg to be ok, got %v", got) + +//// delegate multiple parties +//for i, delegator := range delegators { +//msgDelegate := newTestMsgDelegate(10, sender) +//deliverer.sender = delegator +//got := deliverer.delegate(msgDelegate) +//require.NoError(t, got, "expected msg %d to be ok, got %v", i, got) + +////Check that the account is bonded +//bond := mapper.getDelegatorBond(delegator, sender) +//assert.NotNil(t, bond, "expected delegatee bond %d to exist", bond) +//} + +//// unbond them all +//for i, delegator := range delegators { +//msgUndelegate := NewMsgUnbond(sender, "10") +//deliverer.sender = delegator +//got := deliverer.unbond(msgUndelegate) +//require.NoError(t, got, "expected msg %d to be ok, got %v", i, got) + +////Check that the account is unbonded +//bond := mapper.getDelegatorBond(delegator, sender) +//assert.Nil(t, bond, "expected delegatee bond %d to be nil", bond) +//} +//} + +//func TestVoidCandidacy(t *testing.T) { +//sender, delegator := addrs[0], addrs[1] +//_, _, _, deliverer := createTestInput(t, addrs[0], false, 1000) + +//// create the candidate +//msgDeclareCandidacy := newTestMsgDeclareCandidacy(addrs[0], pks[0], 10) +//got := deliverer.declareCandidacy(msgDeclareCandidacy) +//require.NoError(t, got, "expected no error on runMsgDeclareCandidacy") + +//// bond a delegator +//msgDelegate := newTestMsgDelegate(10, addrs[0]) +//deliverer.sender = delegator +//got = deliverer.delegate(msgDelegate) +//require.NoError(t, got, "expected ok, got %v", got) + +//// unbond the candidates bond portion +//msgUndelegate := NewMsgUnbond(addrs[0], "10") +//deliverer.sender = sender +//got = deliverer.unbond(msgUndelegate) +//require.NoError(t, got, "expected no error on runMsgDeclareCandidacy") + +//// test that this pubkey cannot yet be bonded too +//deliverer.sender = delegator +//got = deliverer.delegate(msgDelegate) +//assert.Error(t, got, "expected error, got %v", got) + +//// test that the delegator can still withdraw their bonds +//got = deliverer.unbond(msgUndelegate) +//require.NoError(t, got, "expected no error on runMsgDeclareCandidacy") + +//// verify that the pubkey can now be reused +//got = deliverer.declareCandidacy(msgDeclareCandidacy) +//assert.NoError(t, got, "expected ok, got %v", got) +//} diff --git a/x/stake/keeper.go b/x/stake/keeper.go new file mode 100644 index 000000000..af2015fe8 --- /dev/null +++ b/x/stake/keeper.go @@ -0,0 +1,300 @@ +package stake + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/wire" + "github.com/cosmos/cosmos-sdk/x/bank" +) + +// keeper of the staking store +type Keeper struct { + storeKey sdk.StoreKey + cdc *wire.Codec + coinKeeper bank.CoinKeeper + + // caches + gs Pool + params Params +} + +func NewKeeper(ctx sdk.Context, cdc *wire.Codec, key sdk.StoreKey, ck bank.CoinKeeper) Keeper { + keeper := Keeper{ + storeKey: key, + cdc: cdc, + coinKeeper: ck, + } + return keeper +} + +//_________________________________________________________________________ + +// get a single candidate +func (k Keeper) GetCandidate(ctx sdk.Context, addr sdk.Address) (candidate Candidate, found bool) { + store := ctx.KVStore(k.storeKey) + b := store.Get(GetCandidateKey(addr)) + if b == nil { + return candidate, false + } + err := k.cdc.UnmarshalBinary(b, &candidate) + if err != nil { + panic(err) + } + return candidate, true +} + +// Get the set of all candidates, retrieve a maxRetrieve number of records +func (k Keeper) GetCandidates(ctx sdk.Context, maxRetrieve int16) (candidates Candidates) { + store := ctx.KVStore(k.storeKey) + iterator := store.Iterator(subspace(CandidatesKey)) + + candidates = make([]Candidate, maxRetrieve) + i := 0 + for ; ; i++ { + if !iterator.Valid() || i > int(maxRetrieve-1) { + iterator.Close() + break + } + bz := iterator.Value() + var candidate Candidate + err := k.cdc.UnmarshalBinary(bz, &candidate) + if err != nil { + panic(err) + } + candidates[i] = candidate + iterator.Next() + } + return candidates[:i] // trim +} + +func (k Keeper) setCandidate(ctx sdk.Context, candidate Candidate) { + store := ctx.KVStore(k.storeKey) + address := candidate.Address + + // retreive the old candidate record + oldCandidate, oldFound := k.GetCandidate(ctx, address) + + // marshal the candidate record and add to the state + bz, err := k.cdc.MarshalBinary(candidate) + if err != nil { + panic(err) + } + store.Set(GetCandidateKey(candidate.Address), bz) + + // mashal the new validator record + validator := Validator{address, candidate.Assets} + bz, err = k.cdc.MarshalBinary(validator) + if err != nil { + panic(err) + } + + // update the list ordered by voting power + if oldFound { + store.Delete(GetValidatorKey(address, oldCandidate.Assets, k.cdc)) + } + store.Set(GetValidatorKey(address, validator.VotingPower, k.cdc), bz) + + // add to the validators to update list if is already a validator + if store.Get(GetRecentValidatorKey(address)) == nil { + return + } + store.Set(GetAccUpdateValidatorKey(validator.Address), bz) + +} + +func (k Keeper) removeCandidate(ctx sdk.Context, address sdk.Address) { + + // first retreive the old candidate record + oldCandidate, found := k.GetCandidate(ctx, address) + if !found { + return + } + + // delete the old candidate record + store := ctx.KVStore(k.storeKey) + store.Delete(GetCandidateKey(address)) + + // delete from recent and power weighted validator groups if the validator + // exists and add validator with zero power to the validator updates + if store.Get(GetRecentValidatorKey(address)) == nil { + return + } + bz, err := k.cdc.MarshalBinary(Validator{address, sdk.ZeroRat}) + if err != nil { + panic(err) + } + store.Set(GetAccUpdateValidatorKey(address), bz) + store.Delete(GetRecentValidatorKey(address)) + store.Delete(GetValidatorKey(address, oldCandidate.Assets, k.cdc)) +} + +//___________________________________________________________________________ + +// get the most recent updated validator set from the Candidates. These bonds +// are already sorted by Assets from the UpdateVotingPower function which +// is the only function which is to modify the Assets +// this function also updaates the most recent validators saved in store +func (k Keeper) GetValidators(ctx sdk.Context) (validators []Validator) { + store := ctx.KVStore(k.storeKey) + + // clear the recent validators store + k.deleteSubSpace(store, RecentValidatorsKey) + + // add the actual validator power sorted store + maxVal := k.GetParams(ctx).MaxValidators + iterator := store.ReverseIterator(subspace(ValidatorsKey)) //smallest to largest + validators = make([]Validator, maxVal) + i := 0 + for ; ; i++ { + if !iterator.Valid() || i > int(maxVal-1) { + iterator.Close() + break + } + bz := iterator.Value() + var val Validator + err := k.cdc.UnmarshalBinary(bz, &val) + if err != nil { + panic(err) + } + validators[i] = val + + // also add to the recent validators group + store.Set(GetRecentValidatorKey(val.Address), bz) + + iterator.Next() + } + + return validators[:i] // trim +} + +// Is the address provided a part of the most recently saved validator group? +func (k Keeper) IsRecentValidator(ctx sdk.Context, address sdk.Address) bool { + store := ctx.KVStore(k.storeKey) + if store.Get(GetRecentValidatorKey(address)) == nil { + return false + } + return true +} + +//_________________________________________________________________________ +// Accumulated updates to the validator set + +// get the most recently updated validators +func (k Keeper) getAccUpdateValidators(ctx sdk.Context) (updates []Validator) { + store := ctx.KVStore(k.storeKey) + + iterator := store.Iterator(subspace(AccUpdateValidatorsKey)) //smallest to largest + for ; iterator.Valid(); iterator.Next() { + valBytes := iterator.Value() + var val Validator + err := k.cdc.UnmarshalBinary(valBytes, &val) + if err != nil { + panic(err) + } + updates = append(updates, val) + } + iterator.Close() + return +} + +// remove all validator update entries +func (k Keeper) clearAccUpdateValidators(ctx sdk.Context) { + store := ctx.KVStore(k.storeKey) + k.deleteSubSpace(store, AccUpdateValidatorsKey) +} + +// TODO move to common functionality somewhere +func (k Keeper) deleteSubSpace(store sdk.KVStore, key []byte) { + iterator := store.Iterator(subspace(key)) + for ; iterator.Valid(); iterator.Next() { + store.Delete(iterator.Key()) + } + iterator.Close() +} + +//_____________________________________________________________________ + +func (k Keeper) getDelegatorBond(ctx sdk.Context, + delegatorAddr, candidateAddr sdk.Address) (bond DelegatorBond, found bool) { + + store := ctx.KVStore(k.storeKey) + delegatorBytes := store.Get(GetDelegatorBondKey(delegatorAddr, candidateAddr, k.cdc)) + if delegatorBytes == nil { + return bond, false + } + + err := k.cdc.UnmarshalBinary(delegatorBytes, &bond) + if err != nil { + panic(err) + } + return bond, true +} + +// load all bonds of a delegator +func (k Keeper) getDelegatorBonds(ctx sdk.Context, delegator sdk.Address, maxRetrieve int16) (bonds []DelegatorBond) { + store := ctx.KVStore(k.storeKey) + delegatorPrefixKey := GetDelegatorBondsKey(delegator, k.cdc) + iterator := store.Iterator(subspace(delegatorPrefixKey)) //smallest to largest + + bonds = make([]DelegatorBond, maxRetrieve) + i := 0 + for ; ; i++ { + if !iterator.Valid() || i > int(maxRetrieve-1) { + iterator.Close() + break + } + bondBytes := iterator.Value() + var bond DelegatorBond + err := k.cdc.UnmarshalBinary(bondBytes, &bond) + if err != nil { + panic(err) + } + bonds[i] = bond + iterator.Next() + } + return bonds[:i] // trim +} + +func (k Keeper) setDelegatorBond(ctx sdk.Context, bond DelegatorBond) { + store := ctx.KVStore(k.storeKey) + b, err := k.cdc.MarshalBinary(bond) + if err != nil { + panic(err) + } + store.Set(GetDelegatorBondKey(bond.DelegatorAddr, bond.CandidateAddr, k.cdc), b) +} + +func (k Keeper) removeDelegatorBond(ctx sdk.Context, bond DelegatorBond) { + store := ctx.KVStore(k.storeKey) + store.Delete(GetDelegatorBondKey(bond.DelegatorAddr, bond.CandidateAddr, k.cdc)) +} + +//_______________________________________________________________________ + +// load/save the global staking params +func (k Keeper) GetParams(ctx sdk.Context) (params Params) { + // check if cached before anything + if k.params != (Params{}) { + return k.params + } + store := ctx.KVStore(k.storeKey) + b := store.Get(ParamKey) + if b == nil { + k.params = defaultParams() + return k.params + } + + err := k.cdc.UnmarshalBinary(b, ¶ms) + if err != nil { + panic(err) + } + return +} +func (k Keeper) setParams(ctx sdk.Context, params Params) { + store := ctx.KVStore(k.storeKey) + b, err := k.cdc.MarshalBinary(params) + if err != nil { + panic(err) + } + store.Set(ParamKey, b) + k.params = Params{} // clear the cache +} diff --git a/x/stake/keeper_keys.go b/x/stake/keeper_keys.go new file mode 100644 index 000000000..051994456 --- /dev/null +++ b/x/stake/keeper_keys.go @@ -0,0 +1,58 @@ +package stake + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/wire" +) + +// TODO remove some of these prefixes once have working multistore + +//nolint +var ( + // Keys for store prefixes + ParamKey = []byte{0x00} // key for global parameters relating to staking + PoolKey = []byte{0x01} // key for global parameters relating to staking + CandidatesKey = []byte{0x02} // prefix for each key to a candidate + ValidatorsKey = []byte{0x03} // prefix for each key to a validator + AccUpdateValidatorsKey = []byte{0x04} // prefix for each key to a validator which is being updated + RecentValidatorsKey = []byte{0x04} // prefix for each key to the last updated validator group + + DelegatorBondKeyPrefix = []byte{0x05} // prefix for each key to a delegator's bond +) + +const maxDigitsForAccount = 12 // ~220,000,000 atoms created at launch + +// get the key for the candidate with address +func GetCandidateKey(addr sdk.Address) []byte { + return append(CandidatesKey, addr.Bytes()...) +} + +// get the key for the validator used in the power-store +func GetValidatorKey(addr sdk.Address, power sdk.Rat, cdc *wire.Codec) []byte { + powerBytes := []byte(power.ToLeftPadded(maxDigitsForAccount)) + return append(ValidatorsKey, append(powerBytes, addr.Bytes()...)...) +} + +// get the key for the accumulated update validators +func GetAccUpdateValidatorKey(addr sdk.Address) []byte { + return append(AccUpdateValidatorsKey, addr.Bytes()...) +} + +// get the key for the accumulated update validators +func GetRecentValidatorKey(addr sdk.Address) []byte { + return append(RecentValidatorsKey, addr.Bytes()...) +} + +// get the key for delegator bond with candidate +func GetDelegatorBondKey(delegatorAddr, candidateAddr sdk.Address, cdc *wire.Codec) []byte { + return append(GetDelegatorBondsKey(delegatorAddr, cdc), candidateAddr.Bytes()...) +} + +// get the prefix for a delegator for all candidates +func GetDelegatorBondsKey(delegatorAddr sdk.Address, cdc *wire.Codec) []byte { + res, err := cdc.MarshalBinary(&delegatorAddr) + if err != nil { + panic(err) + } + return append(DelegatorBondKeyPrefix, res...) +} diff --git a/x/stake/keeper_test.go b/x/stake/keeper_test.go new file mode 100644 index 000000000..6e7478957 --- /dev/null +++ b/x/stake/keeper_test.go @@ -0,0 +1,307 @@ +package stake + +import ( + "bytes" + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + crypto "github.com/tendermint/go-crypto" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var ( + addrDel1 = addrs[0] + addrDel2 = addrs[1] + addrVal1 = addrs[2] + addrVal2 = addrs[3] + addrVal3 = addrs[4] + pk1 = crypto.GenPrivKeyEd25519().PubKey() + pk2 = crypto.GenPrivKeyEd25519().PubKey() + pk3 = crypto.GenPrivKeyEd25519().PubKey() + + candidate1 = Candidate{ + Address: addrVal1, + PubKey: pk1, + Assets: sdk.NewRat(9), + Liabilities: sdk.NewRat(9), + } + candidate2 = Candidate{ + Address: addrVal2, + PubKey: pk2, + Assets: sdk.NewRat(9), + Liabilities: sdk.NewRat(9), + } + candidate3 = Candidate{ + Address: addrVal3, + PubKey: pk3, + Assets: sdk.NewRat(9), + Liabilities: sdk.NewRat(9), + } +) + +// This function tests GetCandidate, GetCandidates, setCandidate, removeCandidate +func TestCandidate(t *testing.T) { + ctx, _, keeper := createTestInput(t, nil, false, 0) + + candidatesEqual := func(c1, c2 Candidate) bool { + return c1.Status == c2.Status && + c1.PubKey.Equals(c2.PubKey) && + bytes.Equal(c1.Address, c2.Address) && + c1.Assets.Equal(c2.Assets) && + c1.Liabilities.Equal(c2.Liabilities) && + c1.Description == c2.Description + } + + // check the empty keeper first + _, found := keeper.GetCandidate(ctx, addrVal1) + assert.False(t, found) + resCands := keeper.GetCandidates(ctx, 100) + assert.Zero(t, len(resCands)) + + // set and retrieve a record + keeper.setCandidate(ctx, candidate1) + resCand, found := keeper.GetCandidate(ctx, addrVal1) + require.True(t, found) + assert.True(t, candidatesEqual(candidate1, resCand), "%v \n %v", resCand, candidate1) + + // modify a records, save, and retrieve + candidate1.Liabilities = sdk.NewRat(99) + keeper.setCandidate(ctx, candidate1) + resCand, found = keeper.GetCandidate(ctx, addrVal1) + require.True(t, found) + assert.True(t, candidatesEqual(candidate1, resCand)) + + // also test that the address has been added to address list + resCands = keeper.GetCandidates(ctx, 100) + require.Equal(t, 1, len(resCands)) + assert.Equal(t, addrVal1, resCands[0].Address) + + // add other candidates + keeper.setCandidate(ctx, candidate2) + keeper.setCandidate(ctx, candidate3) + resCand, found = keeper.GetCandidate(ctx, addrVal2) + require.True(t, found) + assert.True(t, candidatesEqual(candidate2, resCand), "%v \n %v", resCand, candidate2) + resCand, found = keeper.GetCandidate(ctx, addrVal3) + require.True(t, found) + assert.True(t, candidatesEqual(candidate3, resCand), "%v \n %v", resCand, candidate3) + resCands = keeper.GetCandidates(ctx, 100) + require.Equal(t, 3, len(resCands)) + assert.True(t, candidatesEqual(candidate1, resCands[0]), "%v \n %v", resCands[0], candidate1) + assert.True(t, candidatesEqual(candidate2, resCands[1]), "%v \n %v", resCands[1], candidate2) + assert.True(t, candidatesEqual(candidate3, resCands[2]), "%v \n %v", resCands[2], candidate3) + + // remove a record + keeper.removeCandidate(ctx, candidate2.Address) + _, found = keeper.GetCandidate(ctx, addrVal2) + assert.False(t, found) +} + +// tests GetDelegatorBond, GetDelegatorBonds, SetDelegatorBond, removeDelegatorBond +func TestBond(t *testing.T) { + ctx, _, keeper := createTestInput(t, nil, false, 0) + + // first add a candidate1 to delegate too + keeper.setCandidate(ctx, candidate1) + + bond1to1 := DelegatorBond{ + DelegatorAddr: addrDel1, + CandidateAddr: addrVal1, + Shares: sdk.NewRat(9), + } + + bondsEqual := func(b1, b2 DelegatorBond) bool { + return bytes.Equal(b1.DelegatorAddr, b2.DelegatorAddr) && + bytes.Equal(b1.CandidateAddr, b2.CandidateAddr) && + b1.Shares == b2.Shares + } + + // check the empty keeper first + _, found := keeper.getDelegatorBond(ctx, addrDel1, addrVal1) + assert.False(t, found) + + // set and retrieve a record + keeper.setDelegatorBond(ctx, bond1to1) + resBond, found := keeper.getDelegatorBond(ctx, addrDel1, addrVal1) + assert.True(t, found) + assert.True(t, bondsEqual(bond1to1, resBond)) + + // modify a records, save, and retrieve + bond1to1.Shares = sdk.NewRat(99) + keeper.setDelegatorBond(ctx, bond1to1) + resBond, found = keeper.getDelegatorBond(ctx, addrDel1, addrVal1) + assert.True(t, found) + assert.True(t, bondsEqual(bond1to1, resBond)) + + // add some more records + keeper.setCandidate(ctx, candidate2) + keeper.setCandidate(ctx, candidate3) + bond1to2 := DelegatorBond{addrDel1, addrVal2, sdk.NewRat(9)} + bond1to3 := DelegatorBond{addrDel1, addrVal3, sdk.NewRat(9)} + bond2to1 := DelegatorBond{addrDel2, addrVal1, sdk.NewRat(9)} + bond2to2 := DelegatorBond{addrDel2, addrVal2, sdk.NewRat(9)} + bond2to3 := DelegatorBond{addrDel2, addrVal3, sdk.NewRat(9)} + keeper.setDelegatorBond(ctx, bond1to2) + keeper.setDelegatorBond(ctx, bond1to3) + keeper.setDelegatorBond(ctx, bond2to1) + keeper.setDelegatorBond(ctx, bond2to2) + keeper.setDelegatorBond(ctx, bond2to3) + + // test all bond retrieve capabilities + resBonds := keeper.getDelegatorBonds(ctx, addrDel1, 5) + require.Equal(t, 3, len(resBonds)) + assert.True(t, bondsEqual(bond1to1, resBonds[0])) + assert.True(t, bondsEqual(bond1to2, resBonds[1])) + assert.True(t, bondsEqual(bond1to3, resBonds[2])) + resBonds = keeper.getDelegatorBonds(ctx, addrDel1, 3) + require.Equal(t, 3, len(resBonds)) + resBonds = keeper.getDelegatorBonds(ctx, addrDel1, 2) + require.Equal(t, 2, len(resBonds)) + resBonds = keeper.getDelegatorBonds(ctx, addrDel2, 5) + require.Equal(t, 3, len(resBonds)) + assert.True(t, bondsEqual(bond2to1, resBonds[0])) + assert.True(t, bondsEqual(bond2to2, resBonds[1])) + assert.True(t, bondsEqual(bond2to3, resBonds[2])) + + // delete a record + keeper.removeDelegatorBond(ctx, bond2to3) + _, found = keeper.getDelegatorBond(ctx, addrDel2, addrVal3) + assert.False(t, found) + resBonds = keeper.getDelegatorBonds(ctx, addrDel2, 5) + require.Equal(t, 2, len(resBonds)) + assert.True(t, bondsEqual(bond2to1, resBonds[0])) + assert.True(t, bondsEqual(bond2to2, resBonds[1])) + + // delete all the records from delegator 2 + keeper.removeDelegatorBond(ctx, bond2to1) + keeper.removeDelegatorBond(ctx, bond2to2) + _, found = keeper.getDelegatorBond(ctx, addrDel2, addrVal1) + assert.False(t, found) + _, found = keeper.getDelegatorBond(ctx, addrDel2, addrVal2) + assert.False(t, found) + resBonds = keeper.getDelegatorBonds(ctx, addrDel2, 5) + require.Equal(t, 0, len(resBonds)) +} + +// TODO integrate in testing for equal validators, whichever one was a validator +// first remains the validator https://github.com/cosmos/cosmos-sdk/issues/582 +func TestGetValidators(t *testing.T) { + ctx, _, keeper := createTestInput(t, nil, false, 0) + + // initialize some candidates into the state + amts := []int64{0, 100, 1, 400, 200} + n := len(amts) + candidates := make([]Candidate, n) + for i := 0; i < n; i++ { + c := Candidate{ + Status: Unbonded, + PubKey: pks[i], + Address: addrs[i], + Assets: sdk.NewRat(amts[i]), + Liabilities: sdk.NewRat(amts[i]), + } + keeper.setCandidate(ctx, c) + candidates[i] = c + } + + // first make sure everything as normal is ordered + validators := keeper.GetValidators(ctx) + require.Equal(t, len(validators), n) + assert.Equal(t, sdk.NewRat(400), validators[0].VotingPower, "%v", validators) + assert.Equal(t, sdk.NewRat(200), validators[1].VotingPower, "%v", validators) + assert.Equal(t, sdk.NewRat(100), validators[2].VotingPower, "%v", validators) + assert.Equal(t, sdk.NewRat(1), validators[3].VotingPower, "%v", validators) + assert.Equal(t, sdk.NewRat(0), validators[4].VotingPower, "%v", validators) + assert.Equal(t, candidates[3].Address, validators[0].Address, "%v", validators) + assert.Equal(t, candidates[4].Address, validators[1].Address, "%v", validators) + assert.Equal(t, candidates[1].Address, validators[2].Address, "%v", validators) + assert.Equal(t, candidates[2].Address, validators[3].Address, "%v", validators) + assert.Equal(t, candidates[0].Address, validators[4].Address, "%v", validators) + + // test a basic increase in voting power + candidates[3].Assets = sdk.NewRat(500) + keeper.setCandidate(ctx, candidates[3]) + validators = keeper.GetValidators(ctx) + require.Equal(t, len(validators), n) + assert.Equal(t, sdk.NewRat(500), validators[0].VotingPower, "%v", validators) + assert.Equal(t, candidates[3].Address, validators[0].Address, "%v", validators) + + // test a decrease in voting power + candidates[3].Assets = sdk.NewRat(300) + keeper.setCandidate(ctx, candidates[3]) + validators = keeper.GetValidators(ctx) + require.Equal(t, len(validators), n) + assert.Equal(t, sdk.NewRat(300), validators[0].VotingPower, "%v", validators) + assert.Equal(t, candidates[3].Address, validators[0].Address, "%v", validators) + + // test a swap in voting power + candidates[0].Assets = sdk.NewRat(600) + keeper.setCandidate(ctx, candidates[0]) + validators = keeper.GetValidators(ctx) + require.Equal(t, len(validators), n) + assert.Equal(t, sdk.NewRat(600), validators[0].VotingPower, "%v", validators) + assert.Equal(t, candidates[0].Address, validators[0].Address, "%v", validators) + assert.Equal(t, sdk.NewRat(300), validators[1].VotingPower, "%v", validators) + assert.Equal(t, candidates[3].Address, validators[1].Address, "%v", validators) + + // test the max validators term + params := keeper.GetParams(ctx) + n = 2 + params.MaxValidators = uint16(n) + keeper.setParams(ctx, params) + validators = keeper.GetValidators(ctx) + require.Equal(t, len(validators), n) + assert.Equal(t, sdk.NewRat(600), validators[0].VotingPower, "%v", validators) + assert.Equal(t, candidates[0].Address, validators[0].Address, "%v", validators) + assert.Equal(t, sdk.NewRat(300), validators[1].VotingPower, "%v", validators) + assert.Equal(t, candidates[3].Address, validators[1].Address, "%v", validators) +} + +// TODO +// test the mechanism which keeps track of a validator set change +func TestGetAccUpdateValidators(t *testing.T) { + //TODO + // test from nothing to something + // test from something to nothing + // test identical + // test single value change + // test multiple value change + // test validator added at the beginning + // test validator added in the middle + // test validator added at the end + // test multiple validators removed +} + +// clear the tracked changes to the validator set +func TestClearAccUpdateValidators(t *testing.T) { + //TODO +} + +// test if is a validator from the last update +func TestIsRecentValidator(t *testing.T) { + //TODO + + // test that an empty validator set doesn't have any validators + // get the validators for the first time + // test a basic retrieve of something that should be a recent validator + // test a basic retrieve of something that should not be a recent validator + // remove that validator, but don't retrieve the recent validator group + // test that removed validator is not considered a recent validator +} + +func TestParams(t *testing.T) { + ctx, _, keeper := createTestInput(t, nil, false, 0) + expParams := defaultParams() + + //check that the empty keeper loads the default + resParams := keeper.GetParams(ctx) + assert.Equal(t, expParams, resParams) + + //modify a params, save, and retrieve + expParams.MaxValidators = 777 + keeper.setParams(ctx, expParams) + resParams = keeper.GetParams(ctx) + assert.Equal(t, expParams, resParams) +} diff --git a/x/stake/msg.go b/x/stake/msg.go new file mode 100644 index 000000000..28a2edf79 --- /dev/null +++ b/x/stake/msg.go @@ -0,0 +1,227 @@ +package stake + +import ( + "encoding/json" + "fmt" + + sdk "github.com/cosmos/cosmos-sdk/types" + crypto "github.com/tendermint/go-crypto" +) + +// name to idetify transaction types +const MsgType = "stake" + +// XXX remove: think it makes more sense belonging with the Params so we can +// initialize at genesis - to allow for the same tests we should should make +// the ValidateBasic() function a return from an initializable function +// ValidateBasic(bondDenom string) function +const StakingToken = "fermion" + +//Verify interface at compile time +var _, _, _, _ sdk.Msg = &MsgDeclareCandidacy{}, &MsgEditCandidacy{}, &MsgDelegate{}, &MsgUnbond{} + +//______________________________________________________________________ + +// MsgDeclareCandidacy - struct for unbonding transactions +type MsgDeclareCandidacy struct { + Description + CandidateAddr sdk.Address `json:"address"` + PubKey crypto.PubKey `json:"pubkey"` + Bond sdk.Coin `json:"bond"` +} + +func NewMsgDeclareCandidacy(candidateAddr sdk.Address, pubkey crypto.PubKey, + bond sdk.Coin, description Description) MsgDeclareCandidacy { + return MsgDeclareCandidacy{ + Description: description, + CandidateAddr: candidateAddr, + PubKey: pubkey, + Bond: bond, + } +} + +//nolint +func (msg MsgDeclareCandidacy) Type() string { return MsgType } //TODO update "stake/declarecandidacy" +func (msg MsgDeclareCandidacy) Get(key interface{}) (value interface{}) { return nil } +func (msg MsgDeclareCandidacy) GetSigners() []sdk.Address { return []sdk.Address{msg.CandidateAddr} } +func (msg MsgDeclareCandidacy) String() string { + return fmt.Sprintf("CandidateAddr{Address: %v}", msg.CandidateAddr) // XXX fix +} + +// get the bytes for the message signer to sign on +func (msg MsgDeclareCandidacy) GetSignBytes() []byte { + b, err := json.Marshal(msg) + if err != nil { + panic(err) + } + return b +} + +// quick validity check +func (msg MsgDeclareCandidacy) ValidateBasic() sdk.Error { + if msg.CandidateAddr == nil { + return ErrCandidateEmpty() + } + if msg.Bond.Denom != StakingToken { + return ErrBadBondingDenom() + } + if msg.Bond.Amount <= 0 { + return ErrBadBondingAmount() + // return sdk.ErrInvalidCoins(sdk.Coins{msg.Bond}.String()) + } + empty := Description{} + if msg.Description == empty { + return newError(CodeInvalidInput, "description must be included") + } + return nil +} + +//______________________________________________________________________ + +// MsgEditCandidacy - struct for editing a candidate +type MsgEditCandidacy struct { + Description + CandidateAddr sdk.Address `json:"address"` +} + +func NewMsgEditCandidacy(candidateAddr sdk.Address, description Description) MsgEditCandidacy { + return MsgEditCandidacy{ + Description: description, + CandidateAddr: candidateAddr, + } +} + +//nolint +func (msg MsgEditCandidacy) Type() string { return MsgType } //TODO update "stake/msgeditcandidacy" +func (msg MsgEditCandidacy) Get(key interface{}) (value interface{}) { return nil } +func (msg MsgEditCandidacy) GetSigners() []sdk.Address { return []sdk.Address{msg.CandidateAddr} } +func (msg MsgEditCandidacy) String() string { + return fmt.Sprintf("CandidateAddr{Address: %v}", msg.CandidateAddr) // XXX fix +} + +// get the bytes for the message signer to sign on +func (msg MsgEditCandidacy) GetSignBytes() []byte { + b, err := json.Marshal(msg) + if err != nil { + panic(err) + } + return b +} + +// quick validity check +func (msg MsgEditCandidacy) ValidateBasic() sdk.Error { + if msg.CandidateAddr == nil { + return ErrCandidateEmpty() + } + empty := Description{} + if msg.Description == empty { + return newError(CodeInvalidInput, "Transaction must include some information to modify") + } + return nil +} + +//______________________________________________________________________ + +// MsgDelegate - struct for bonding transactions +type MsgDelegate struct { + DelegatorAddr sdk.Address `json:"address"` + CandidateAddr sdk.Address `json:"address"` + Bond sdk.Coin `json:"bond"` +} + +func NewMsgDelegate(delegatorAddr, candidateAddr sdk.Address, bond sdk.Coin) MsgDelegate { + return MsgDelegate{ + DelegatorAddr: delegatorAddr, + CandidateAddr: candidateAddr, + Bond: bond, + } +} + +//nolint +func (msg MsgDelegate) Type() string { return MsgType } //TODO update "stake/msgeditcandidacy" +func (msg MsgDelegate) Get(key interface{}) (value interface{}) { return nil } +func (msg MsgDelegate) GetSigners() []sdk.Address { return []sdk.Address{msg.DelegatorAddr} } +func (msg MsgDelegate) String() string { + return fmt.Sprintf("Addr{Address: %v}", msg.DelegatorAddr) // XXX fix +} + +// get the bytes for the message signer to sign on +func (msg MsgDelegate) GetSignBytes() []byte { + b, err := json.Marshal(msg) + if err != nil { + panic(err) + } + return b +} + +// quick validity check +func (msg MsgDelegate) ValidateBasic() sdk.Error { + if msg.DelegatorAddr == nil { + return ErrBadDelegatorAddr() + } + if msg.CandidateAddr == nil { + return ErrBadCandidateAddr() + } + if msg.Bond.Denom != StakingToken { + return ErrBadBondingDenom() + } + if msg.Bond.Amount <= 0 { + return ErrBadBondingAmount() + // return sdk.ErrInvalidCoins(sdk.Coins{msg.Bond}.String()) + } + return nil +} + +//______________________________________________________________________ + +// MsgUnbond - struct for unbonding transactions +type MsgUnbond struct { + DelegatorAddr sdk.Address `json:"address"` + CandidateAddr sdk.Address `json:"address"` + Shares string `json:"shares"` +} + +func NewMsgUnbond(delegatorAddr, candidateAddr sdk.Address, shares string) MsgUnbond { + return MsgUnbond{ + DelegatorAddr: delegatorAddr, + CandidateAddr: candidateAddr, + Shares: shares, + } +} + +//nolint +func (msg MsgUnbond) Type() string { return MsgType } //TODO update "stake/msgeditcandidacy" +func (msg MsgUnbond) Get(key interface{}) (value interface{}) { return nil } +func (msg MsgUnbond) GetSigners() []sdk.Address { return []sdk.Address{msg.DelegatorAddr} } +func (msg MsgUnbond) String() string { + return fmt.Sprintf("Addr{Address: %v}", msg.DelegatorAddr) // XXX fix +} + +// get the bytes for the message signer to sign on +func (msg MsgUnbond) GetSignBytes() []byte { + b, err := json.Marshal(msg) + if err != nil { + panic(err) + } + return b +} + +// quick validity check +func (msg MsgUnbond) ValidateBasic() sdk.Error { + if msg.DelegatorAddr == nil { + return ErrBadDelegatorAddr() + } + if msg.CandidateAddr == nil { + return ErrBadCandidateAddr() + } + if msg.Shares != "MAX" { + rat, err := sdk.NewRatFromDecimal(msg.Shares) + if err != nil { + return ErrBadShares() + } + if rat.IsZero() || rat.LT(sdk.ZeroRat) { + return ErrBadShares() + } + } + return nil +} diff --git a/x/stake/msg_test.go b/x/stake/msg_test.go new file mode 100644 index 000000000..b3e2bbe3a --- /dev/null +++ b/x/stake/msg_test.go @@ -0,0 +1,156 @@ +package stake + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + sdk "github.com/cosmos/cosmos-sdk/types" + crypto "github.com/tendermint/go-crypto" +) + +var ( + coinPos = sdk.Coin{"fermion", 1000} + coinZero = sdk.Coin{"fermion", 0} + coinNeg = sdk.Coin{"fermion", -10000} + coinPosNotAtoms = sdk.Coin{"foo", 10000} + coinZeroNotAtoms = sdk.Coin{"foo", 0} + coinNegNotAtoms = sdk.Coin{"foo", -10000} +) + +// test ValidateBasic for MsgDeclareCandidacy +func TestMsgDeclareCandidacy(t *testing.T) { + tests := []struct { + name, moniker, identity, website, details string + candidateAddr sdk.Address + pubkey crypto.PubKey + bond sdk.Coin + expectPass bool + }{ + {"basic good", "a", "b", "c", "d", addrs[0], pks[0], coinPos, true}, + {"partial description", "", "", "c", "", addrs[0], pks[0], coinPos, true}, + {"empty description", "", "", "", "", addrs[0], pks[0], coinPos, false}, + {"empty address", "a", "b", "c", "d", emptyAddr, pks[0], coinPos, false}, + {"empty pubkey", "a", "b", "c", "d", addrs[0], emptyPubkey, coinPos, true}, + {"empty bond", "a", "b", "c", "d", addrs[0], pks[0], coinZero, false}, + {"negative bond", "a", "b", "c", "d", addrs[0], pks[0], coinNeg, false}, + {"negative bond", "a", "b", "c", "d", addrs[0], pks[0], coinNeg, false}, + {"wrong staking token", "a", "b", "c", "d", addrs[0], pks[0], coinPosNotAtoms, false}, + } + + for _, tc := range tests { + description := NewDescription(tc.moniker, tc.identity, tc.website, tc.details) + msg := NewMsgDeclareCandidacy(tc.candidateAddr, tc.pubkey, tc.bond, description) + if tc.expectPass { + assert.Nil(t, msg.ValidateBasic(), "test: %v", tc.name) + } else { + assert.NotNil(t, msg.ValidateBasic(), "test: %v", tc.name) + } + } +} + +// test ValidateBasic for MsgEditCandidacy +func TestMsgEditCandidacy(t *testing.T) { + tests := []struct { + name, moniker, identity, website, details string + candidateAddr sdk.Address + expectPass bool + }{ + {"basic good", "a", "b", "c", "d", addrs[0], true}, + {"partial description", "", "", "c", "", addrs[0], true}, + {"empty description", "", "", "", "", addrs[0], false}, + {"empty address", "a", "b", "c", "d", emptyAddr, false}, + } + + for _, tc := range tests { + description := NewDescription(tc.moniker, tc.identity, tc.website, tc.details) + msg := NewMsgEditCandidacy(tc.candidateAddr, description) + if tc.expectPass { + assert.Nil(t, msg.ValidateBasic(), "test: %v", tc.name) + } else { + assert.NotNil(t, msg.ValidateBasic(), "test: %v", tc.name) + } + } +} + +// test ValidateBasic for MsgDelegate +func TestMsgDelegate(t *testing.T) { + tests := []struct { + name string + delegatorAddr sdk.Address + candidateAddr sdk.Address + bond sdk.Coin + expectPass bool + }{ + {"basic good", addrs[0], addrs[1], coinPos, true}, + {"self bond", addrs[0], addrs[0], coinPos, true}, + {"empty delegator", emptyAddr, addrs[0], coinPos, false}, + {"empty candidate", addrs[0], emptyAddr, coinPos, false}, + {"empty bond", addrs[0], addrs[1], coinZero, false}, + {"negative bond", addrs[0], addrs[1], coinNeg, false}, + {"wrong staking token", addrs[0], addrs[1], coinPosNotAtoms, false}, + } + + for _, tc := range tests { + msg := NewMsgDelegate(tc.delegatorAddr, tc.candidateAddr, tc.bond) + if tc.expectPass { + assert.Nil(t, msg.ValidateBasic(), "test: %v", tc.name) + } else { + assert.NotNil(t, msg.ValidateBasic(), "test: %v", tc.name) + } + } +} + +// test ValidateBasic for MsgUnbond +func TestMsgUnbond(t *testing.T) { + tests := []struct { + name string + delegatorAddr sdk.Address + candidateAddr sdk.Address + shares string + expectPass bool + }{ + {"max unbond", addrs[0], addrs[1], "MAX", true}, + {"decimal unbond", addrs[0], addrs[1], "0.1", true}, + {"negative decimal unbond", addrs[0], addrs[1], "-0.1", false}, + {"zero unbond", addrs[0], addrs[1], "0.0", false}, + {"invalid decimal", addrs[0], addrs[0], "sunny", false}, + {"empty delegator", emptyAddr, addrs[0], "0.1", false}, + {"empty candidate", addrs[0], emptyAddr, "0.1", false}, + } + + for _, tc := range tests { + msg := NewMsgUnbond(tc.delegatorAddr, tc.candidateAddr, tc.shares) + if tc.expectPass { + assert.Nil(t, msg.ValidateBasic(), "test: %v", tc.name) + } else { + assert.NotNil(t, msg.ValidateBasic(), "test: %v", tc.name) + } + } +} + +// TODO introduce with go-amino +//func TestSerializeMsg(t *testing.T) { + +//// make sure all types construct properly +//bondAmt := 1234321 +//bond := sdk.Coin{Denom: "atom", Amount: int64(bondAmt)} + +//tests := []struct { +//tx sdk.Msg +//}{ +//{NewMsgDeclareCandidacy(addrs[0], pks[0], bond, Description{})}, +//{NewMsgEditCandidacy(addrs[0], Description{})}, +//{NewMsgDelegate(addrs[0], addrs[1], bond)}, +//{NewMsgUnbond(addrs[0], addrs[1], strconv.Itoa(bondAmt))}, +//} + +//for i, tc := range tests { +//var tx sdk.Tx +//bs := wire.BinaryBytes(tc.tx) +//err := wire.ReadBinaryBytes(bs, &tx) +//if assert.NoError(t, err, "%d", i) { +//assert.Equal(t, tc.tx, tx, "%d", i) +//} +//} +//} diff --git a/x/stake/pool.go b/x/stake/pool.go new file mode 100644 index 000000000..4c185580e --- /dev/null +++ b/x/stake/pool.go @@ -0,0 +1,135 @@ +package stake + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// load/save the global staking state +func (k Keeper) GetPool(ctx sdk.Context) (gs Pool) { + // check if cached before anything + if k.gs != (Pool{}) { + return k.gs + } + store := ctx.KVStore(k.storeKey) + b := store.Get(PoolKey) + if b == nil { + return initialPool() + } + err := k.cdc.UnmarshalBinary(b, &gs) + if err != nil { + panic(err) // This error should never occur big problem if does + } + return +} + +func (k Keeper) setPool(ctx sdk.Context, p Pool) { + store := ctx.KVStore(k.storeKey) + b, err := k.cdc.MarshalBinary(p) + if err != nil { + panic(err) + } + store.Set(PoolKey, b) + k.gs = Pool{} // clear the cache +} + +//_______________________________________________________________________ + +//TODO make these next two functions more efficient should be reading and writting to state ye know + +// move a candidates asset pool from bonded to unbonded pool +func (k Keeper) bondedToUnbondedPool(ctx sdk.Context, candidate Candidate) { + + // replace bonded shares with unbonded shares + tokens := k.removeSharesBonded(ctx, candidate.Assets) + candidate.Assets = k.addTokensUnbonded(ctx, tokens) + candidate.Status = Unbonded + k.setCandidate(ctx, candidate) +} + +// move a candidates asset pool from unbonded to bonded pool +func (k Keeper) unbondedToBondedPool(ctx sdk.Context, candidate Candidate) { + + // replace unbonded shares with bonded shares + tokens := k.removeSharesUnbonded(ctx, candidate.Assets) + candidate.Assets = k.addTokensBonded(ctx, tokens) + candidate.Status = Bonded + k.setCandidate(ctx, candidate) +} + +//_______________________________________________________________________ + +func (k Keeper) addTokensBonded(ctx sdk.Context, amount int64) (issuedShares sdk.Rat) { + p := k.GetPool(ctx) + issuedShares = p.bondedShareExRate().Inv().Mul(sdk.NewRat(amount)) // (tokens/shares)^-1 * tokens + p.BondedPool += amount + p.BondedShares = p.BondedShares.Add(issuedShares) + k.setPool(ctx, p) + return +} + +func (k Keeper) removeSharesBonded(ctx sdk.Context, shares sdk.Rat) (removedTokens int64) { + p := k.GetPool(ctx) + removedTokens = p.bondedShareExRate().Mul(shares).Evaluate() // (tokens/shares) * shares + p.BondedShares = p.BondedShares.Sub(shares) + p.BondedPool -= removedTokens + k.setPool(ctx, p) + return +} + +func (k Keeper) addTokensUnbonded(ctx sdk.Context, amount int64) (issuedShares sdk.Rat) { + p := k.GetPool(ctx) + issuedShares = p.unbondedShareExRate().Inv().Mul(sdk.NewRat(amount)) // (tokens/shares)^-1 * tokens + p.UnbondedShares = p.UnbondedShares.Add(issuedShares) + p.UnbondedPool += amount + k.setPool(ctx, p) + return +} + +func (k Keeper) removeSharesUnbonded(ctx sdk.Context, shares sdk.Rat) (removedTokens int64) { + p := k.GetPool(ctx) + removedTokens = p.unbondedShareExRate().Mul(shares).Evaluate() // (tokens/shares) * shares + p.UnbondedShares = p.UnbondedShares.Sub(shares) + p.UnbondedPool -= removedTokens + k.setPool(ctx, p) + return +} + +//_______________________________________________________________________ + +// add tokens to a candidate +func (k Keeper) candidateAddTokens(ctx sdk.Context, candidate Candidate, amount int64) (issuedDelegatorShares sdk.Rat) { + + p := k.GetPool(ctx) + exRate := candidate.delegatorShareExRate() + + var receivedGlobalShares sdk.Rat + if candidate.Status == Bonded { + receivedGlobalShares = k.addTokensBonded(ctx, amount) + } else { + receivedGlobalShares = k.addTokensUnbonded(ctx, amount) + } + candidate.Assets = candidate.Assets.Add(receivedGlobalShares) + + issuedDelegatorShares = exRate.Mul(receivedGlobalShares) + candidate.Liabilities = candidate.Liabilities.Add(issuedDelegatorShares) + k.setPool(ctx, p) // TODO cache Pool? + return +} + +// remove shares from a candidate +func (k Keeper) candidateRemoveShares(ctx sdk.Context, candidate Candidate, shares sdk.Rat) (createdCoins int64) { + + p := k.GetPool(ctx) + //exRate := candidate.delegatorShareExRate() //XXX make sure not used + + globalPoolSharesToRemove := candidate.delegatorShareExRate().Mul(shares) + if candidate.Status == Bonded { + createdCoins = k.removeSharesBonded(ctx, globalPoolSharesToRemove) + } else { + createdCoins = k.removeSharesUnbonded(ctx, globalPoolSharesToRemove) + } + candidate.Assets = candidate.Assets.Sub(globalPoolSharesToRemove) + candidate.Liabilities = candidate.Liabilities.Sub(shares) + k.setPool(ctx, p) // TODO cache Pool? + return +} diff --git a/x/stake/pool_test.go b/x/stake/pool_test.go new file mode 100644 index 000000000..760a89a16 --- /dev/null +++ b/x/stake/pool_test.go @@ -0,0 +1,22 @@ +package stake + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestPool(t *testing.T) { + ctx, _, keeper := createTestInput(t, nil, false, 0) + expPool := initialPool() + + //check that the empty keeper loads the default + resPool := keeper.GetPool(ctx) + assert.Equal(t, expPool, resPool) + + //modify a params, save, and retrieve + expPool.TotalSupply = 777 + keeper.setPool(ctx, expPool) + resPool = keeper.GetPool(ctx) + assert.Equal(t, expPool, resPool) +} diff --git a/x/stake/test_common.go b/x/stake/test_common.go new file mode 100644 index 000000000..aef425581 --- /dev/null +++ b/x/stake/test_common.go @@ -0,0 +1,156 @@ +package stake + +import ( + "encoding/hex" + "testing" + + "github.com/stretchr/testify/require" + + abci "github.com/tendermint/abci/types" + crypto "github.com/tendermint/go-crypto" + oldwire "github.com/tendermint/go-wire" + dbm "github.com/tendermint/tmlibs/db" + + "github.com/cosmos/cosmos-sdk/examples/basecoin/types" + "github.com/cosmos/cosmos-sdk/store" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/wire" + "github.com/cosmos/cosmos-sdk/x/auth" + "github.com/cosmos/cosmos-sdk/x/bank" +) + +// dummy addresses used for testing +var ( + addrs = []sdk.Address{ + testAddr("A58856F0FD53BF058B4909A21AEC019107BA6160"), + testAddr("A58856F0FD53BF058B4909A21AEC019107BA6161"), + testAddr("A58856F0FD53BF058B4909A21AEC019107BA6162"), + testAddr("A58856F0FD53BF058B4909A21AEC019107BA6163"), + testAddr("A58856F0FD53BF058B4909A21AEC019107BA6164"), + testAddr("A58856F0FD53BF058B4909A21AEC019107BA6165"), + testAddr("A58856F0FD53BF058B4909A21AEC019107BA6166"), + testAddr("A58856F0FD53BF058B4909A21AEC019107BA6167"), + testAddr("A58856F0FD53BF058B4909A21AEC019107BA6168"), + testAddr("A58856F0FD53BF058B4909A21AEC019107BA6169"), + } + + // dummy pubkeys used for testing + pks = []crypto.PubKey{ + newPubKey("0B485CFC0EECC619440448436F8FC9DF40566F2369E72400281454CB552AFB50"), + newPubKey("0B485CFC0EECC619440448436F8FC9DF40566F2369E72400281454CB552AFB51"), + newPubKey("0B485CFC0EECC619440448436F8FC9DF40566F2369E72400281454CB552AFB52"), + newPubKey("0B485CFC0EECC619440448436F8FC9DF40566F2369E72400281454CB552AFB53"), + newPubKey("0B485CFC0EECC619440448436F8FC9DF40566F2369E72400281454CB552AFB54"), + newPubKey("0B485CFC0EECC619440448436F8FC9DF40566F2369E72400281454CB552AFB55"), + newPubKey("0B485CFC0EECC619440448436F8FC9DF40566F2369E72400281454CB552AFB56"), + newPubKey("0B485CFC0EECC619440448436F8FC9DF40566F2369E72400281454CB552AFB57"), + newPubKey("0B485CFC0EECC619440448436F8FC9DF40566F2369E72400281454CB552AFB58"), + newPubKey("0B485CFC0EECC619440448436F8FC9DF40566F2369E72400281454CB552AFB59"), + } + + emptyAddr sdk.Address + emptyPubkey crypto.PubKey +) + +// XXX reference the common declaration of this function +func subspace(prefix []byte) (start, end []byte) { + end = make([]byte, len(prefix)) + copy(end, prefix) + end[len(end)-1]++ + return prefix, end +} + +// custom tx codec +// TODO: use new go-wire +func makeTestCodec() *wire.Codec { + + const msgTypeSend = 0x1 + const msgTypeIssue = 0x2 + const msgTypeDeclareCandidacy = 0x3 + const msgTypeEditCandidacy = 0x4 + const msgTypeDelegate = 0x5 + const msgTypeUnbond = 0x6 + var _ = oldwire.RegisterInterface( + struct{ sdk.Msg }{}, + oldwire.ConcreteType{bank.SendMsg{}, msgTypeSend}, + oldwire.ConcreteType{bank.IssueMsg{}, msgTypeIssue}, + oldwire.ConcreteType{MsgDeclareCandidacy{}, msgTypeDeclareCandidacy}, + oldwire.ConcreteType{MsgEditCandidacy{}, msgTypeEditCandidacy}, + oldwire.ConcreteType{MsgDelegate{}, msgTypeDelegate}, + oldwire.ConcreteType{MsgUnbond{}, msgTypeUnbond}, + ) + + const accTypeApp = 0x1 + var _ = oldwire.RegisterInterface( + struct{ sdk.Account }{}, + oldwire.ConcreteType{&types.AppAccount{}, accTypeApp}, + ) + cdc := wire.NewCodec() + + // cdc.RegisterInterface((*sdk.Msg)(nil), nil) + // bank.RegisterWire(cdc) // Register bank.[SendMsg,IssueMsg] types. + // crypto.RegisterWire(cdc) // Register crypto.[PubKey,PrivKey,Signature] types. + return cdc +} + +func paramsNoInflation() Params { + return Params{ + InflationRateChange: sdk.ZeroRat, + InflationMax: sdk.ZeroRat, + InflationMin: sdk.ZeroRat, + GoalBonded: sdk.NewRat(67, 100), + MaxValidators: 100, + BondDenom: "fermion", + } +} + +// hogpodge of all sorts of input required for testing +func createTestInput(t *testing.T, sender sdk.Address, isCheckTx bool, initCoins int64) (sdk.Context, sdk.AccountMapper, Keeper) { + db := dbm.NewMemDB() + keyStake := sdk.NewKVStoreKey("stake") + keyMain := keyStake //sdk.NewKVStoreKey("main") //TODO fix multistore + + ms := store.NewCommitMultiStore(db) + ms.MountStoreWithDB(keyStake, sdk.StoreTypeIAVL, db) + err := ms.LoadLatestVersion() + require.Nil(t, err) + + ctx := sdk.NewContext(ms, abci.Header{ChainID: "foochainid"}, isCheckTx, nil) + cdc := makeTestCodec() + accountMapper := auth.NewAccountMapperSealed( + keyMain, // target store + &auth.BaseAccount{}, // prototype + ) + ck := bank.NewCoinKeeper(accountMapper) + keeper := NewKeeper(ctx, cdc, keyStake, ck) + + //params := paramsNoInflation() + params := keeper.GetParams(ctx) + + // fill all the addresses with some coins + for _, addr := range addrs { + ck.AddCoins(ctx, addr, sdk.Coins{{params.BondDenom, initCoins}}) + } + + return ctx, accountMapper, keeper +} + +func newPubKey(pk string) (res crypto.PubKey) { + pkBytes, err := hex.DecodeString(pk) + if err != nil { + panic(err) + } + //res, err = crypto.PubKeyFromBytes(pkBytes) + var pkEd crypto.PubKeyEd25519 + copy(pkEd[:], pkBytes[:]) + return pkEd.Wrap() +} + +// for incode address generation +func testAddr(addr string) sdk.Address { + res, err := sdk.GetAddress(addr) + if err != nil { + panic(err) + } + return res +} diff --git a/x/stake/tick.go b/x/stake/tick.go new file mode 100644 index 000000000..6aa2da95d --- /dev/null +++ b/x/stake/tick.go @@ -0,0 +1,79 @@ +package stake + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + abci "github.com/tendermint/abci/types" +) + +const ( + hrsPerYear = 8766 // as defined by a julian year of 365.25 days + precision = 1000000000 +) + +var hrsPerYrRat = sdk.NewRat(hrsPerYear) // as defined by a julian year of 365.25 days + +// Tick - called at the end of every block +func (k Keeper) Tick(ctx sdk.Context) (change []*abci.Validator, err error) { + + // retrieve params + p := k.GetPool(ctx) + height := ctx.BlockHeight() + + // Process Validator Provisions + // XXX right now just process every 5 blocks, in new SDK make hourly + if p.InflationLastTime+5 <= height { + p.InflationLastTime = height + k.processProvisions(ctx) + } + + newVals := k.GetValidators(ctx) + + // XXX determine change from old validators, set to change + _ = newVals + return change, nil +} + +// process provisions for an hour period +func (k Keeper) processProvisions(ctx sdk.Context) { + + pool := k.GetPool(ctx) + pool.Inflation = k.nextInflation(ctx).Round(precision) + + // Because the validators hold a relative bonded share (`GlobalStakeShare`), when + // more bonded tokens are added proportionally to all validators the only term + // which needs to be updated is the `BondedPool`. So for each previsions cycle: + + provisions := pool.Inflation.Mul(sdk.NewRat(pool.TotalSupply)).Quo(hrsPerYrRat).Evaluate() + pool.BondedPool += provisions + pool.TotalSupply += provisions + + // save the params + k.setPool(ctx, pool) +} + +// get the next inflation rate for the hour +func (k Keeper) nextInflation(ctx sdk.Context) (inflation sdk.Rat) { + + params := k.GetParams(ctx) + pool := k.GetPool(ctx) + // The target annual inflation rate is recalculated for each previsions cycle. The + // inflation is also subject to a rate change (positive of negative) depending or + // the distance from the desired ratio (67%). The maximum rate change possible is + // defined to be 13% per year, however the annual inflation is capped as between + // 7% and 20%. + + // (1 - bondedRatio/GoalBonded) * InflationRateChange + inflationRateChangePerYear := sdk.OneRat.Sub(pool.bondedRatio().Quo(params.GoalBonded)).Mul(params.InflationRateChange) + inflationRateChange := inflationRateChangePerYear.Quo(hrsPerYrRat) + + // increase the new annual inflation for this next cycle + inflation = pool.Inflation.Add(inflationRateChange) + if inflation.GT(params.InflationMax) { + inflation = params.InflationMax + } + if inflation.LT(params.InflationMin) { + inflation = params.InflationMin + } + + return +} diff --git a/x/stake/tick_test.go b/x/stake/tick_test.go new file mode 100644 index 000000000..540ce4699 --- /dev/null +++ b/x/stake/tick_test.go @@ -0,0 +1,116 @@ +package stake + +//import ( +//"testing" + +//sdk "github.com/cosmos/cosmos-sdk/types" +//"github.com/stretchr/testify/assert" +//) + +//func TestGetInflation(t *testing.T) { +//ctx, _, keeper := createTestInput(t, nil, false, 0) +//params := defaultParams() +//keeper.setParams(ctx, params) +//gs := keeper.GetPool(ctx) + +//// Governing Mechanism: +//// bondedRatio = BondedPool / TotalSupply +//// inflationRateChangePerYear = (1- bondedRatio/ GoalBonded) * MaxInflationRateChange + +//tests := []struct { +//setBondedPool, setTotalSupply int64 +//setInflation, expectedChange sdk.Rat +//}{ +//// with 0% bonded atom supply the inflation should increase by InflationRateChange +//{0, 0, sdk.NewRat(7, 100), params.InflationRateChange.Quo(hrsPerYr)}, + +//// 100% bonded, starting at 20% inflation and being reduced +//{1, 1, sdk.NewRat(20, 100), sdk.OneRat.Sub(sdk.OneRat.Quo(params.GoalBonded)).Mul(params.InflationRateChange).Quo(hrsPerYr)}, + +//// 50% bonded, starting at 10% inflation and being increased +//{1, 2, sdk.NewRat(10, 100), sdk.OneRat.Sub(sdk.NewRat(1, 2).Quo(params.GoalBonded)).Mul(params.InflationRateChange).Quo(hrsPerYr)}, + +//// test 7% minimum stop (testing with 100% bonded) +//{1, 1, sdk.NewRat(7, 100), sdk.ZeroRat}, +//{1, 1, sdk.NewRat(70001, 1000000), sdk.NewRat(-1, 1000000)}, + +//// test 20% maximum stop (testing with 0% bonded) +//{0, 0, sdk.NewRat(20, 100), sdk.ZeroRat}, +//{0, 0, sdk.NewRat(199999, 1000000), sdk.NewRat(1, 1000000)}, + +//// perfect balance shouldn't change inflation +//{67, 100, sdk.NewRat(15, 100), sdk.ZeroRat}, +//} +//for _, tc := range tests { +//gs.BondedPool, p.TotalSupply = tc.setBondedPool, tc.setTotalSupply +//gs.Inflation = tc.setInflation + +//inflation := nextInflation(gs, params) +//diffInflation := inflation.Sub(tc.setInflation) + +//assert.True(t, diffInflation.Equal(tc.expectedChange), +//"%v, %v", diffInflation, tc.expectedChange) +//} +//} + +//func TestProcessProvisions(t *testing.T) { +//ctx, _, keeper := createTestInput(t, nil, false, 0) +//params := defaultParams() +//keeper.setParams(ctx, params) +//gs := keeper.GetPool(ctx) + +//// create some candidates some bonded, some unbonded +//candidates := candidatesFromAddrsEmpty(addrs) +//for i, candidate := range candidates { +//if i < 5 { +//candidate.Status = Bonded +//} +//mintedTokens := int64((i + 1) * 10000000) +//gs.TotalSupply += mintedTokens +//keeper.candidateAddTokens(ctx, candidate, mintedTokens) +//keeper.setCandidate(ctx, candidate) +//} +//var totalSupply int64 = 550000000 +//var bondedShares int64 = 150000000 +//var unbondedShares int64 = 400000000 + +//// initial bonded ratio ~ 27% +//assert.True(t, p.bondedRatio().Equal(sdk.NewRat(bondedShares, totalSupply)), "%v", p.bondedRatio()) + +//// Supplies +//assert.Equal(t, totalSupply, p.TotalSupply) +//assert.Equal(t, bondedShares, p.BondedPool) +//assert.Equal(t, unbondedShares, p.UnbondedPool) + +//// test the value of candidate shares +//assert.True(t, p.bondedShareExRate().Equal(sdk.OneRat), "%v", p.bondedShareExRate()) + +//initialSupply := p.TotalSupply +//initialUnbonded := p.TotalSupply - p.BondedPool + +//// process the provisions a year +//for hr := 0; hr < 8766; hr++ { +//expInflation := nextInflation(gs, params).Round(1000000000) +//expProvisions := (expInflation.Mul(sdk.NewRat(gs.TotalSupply)).Quo(hrsPerYr)).Evaluate() +//startBondedPool := p.BondedPool +//startTotalSupply := p.TotalSupply +//processProvisions(ctx, keeper, p, params) +//assert.Equal(t, startBondedPool+expProvisions, p.BondedPool) +//assert.Equal(t, startTotalSupply+expProvisions, p.TotalSupply) +//} +//assert.NotEqual(t, initialSupply, p.TotalSupply) +//assert.Equal(t, initialUnbonded, p.UnbondedPool) +////panic(fmt.Sprintf("debug total %v, bonded %v, diff %v\n", p.TotalSupply, p.BondedPool, p.TotalSupply-gs.BondedPool)) + +//// initial bonded ratio ~ 35% ~ 30% increase for bonded holders +//assert.True(t, p.bondedRatio().Equal(sdk.NewRat(105906511, 305906511)), "%v", p.bondedRatio()) + +//// global supply +//assert.Equal(t, int64(611813022), p.TotalSupply) +//assert.Equal(t, int64(211813022), p.BondedPool) +//assert.Equal(t, unbondedShares, p.UnbondedPool) + +//// test the value of candidate shares +//assert.True(t, p.bondedShareExRate().Mul(sdk.NewRat(bondedShares)).Equal(sdk.NewRat(211813022)), "%v", p.bondedShareExRate()) + +//} diff --git a/x/stake/types.go b/x/stake/types.go new file mode 100644 index 000000000..2799e1d76 --- /dev/null +++ b/x/stake/types.go @@ -0,0 +1,197 @@ +package stake + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + crypto "github.com/tendermint/go-crypto" +) + +// Params defines the high level settings for staking +type Params struct { + InflationRateChange sdk.Rat `json:"inflation_rate_change"` // maximum annual change in inflation rate + InflationMax sdk.Rat `json:"inflation_max"` // maximum inflation rate + InflationMin sdk.Rat `json:"inflation_min"` // minimum inflation rate + GoalBonded sdk.Rat `json:"goal_bonded"` // Goal of percent bonded atoms + + MaxValidators uint16 `json:"max_validators"` // maximum number of validators + BondDenom string `json:"bond_denom"` // bondable coin denomination +} + +// XXX do we want to allow for default params even or do we want to enforce that you +// need to be explicit about defining all params in genesis? +func defaultParams() Params { + return Params{ + InflationRateChange: sdk.NewRat(13, 100), + InflationMax: sdk.NewRat(20, 100), + InflationMin: sdk.NewRat(7, 100), + GoalBonded: sdk.NewRat(67, 100), + MaxValidators: 100, + BondDenom: "fermion", + } +} + +//_________________________________________________________________________ + +// Pool - dynamic parameters of the current state +type Pool struct { + TotalSupply int64 `json:"total_supply"` // total supply of all tokens + BondedShares sdk.Rat `json:"bonded_shares"` // sum of all shares distributed for the Bonded Pool + UnbondedShares sdk.Rat `json:"unbonded_shares"` // sum of all shares distributed for the Unbonded Pool + BondedPool int64 `json:"bonded_pool"` // reserve of bonded tokens + UnbondedPool int64 `json:"unbonded_pool"` // reserve of unbonded tokens held with candidates + InflationLastTime int64 `json:"inflation_last_time"` // block which the last inflation was processed // TODO make time + Inflation sdk.Rat `json:"inflation"` // current annual inflation rate +} + +// XXX define globalstate interface? + +func initialPool() Pool { + return Pool{ + TotalSupply: 0, + BondedShares: sdk.ZeroRat, + UnbondedShares: sdk.ZeroRat, + BondedPool: 0, + UnbondedPool: 0, + InflationLastTime: 0, + Inflation: sdk.NewRat(7, 100), + } +} + +// get the bond ratio of the global state +func (p Pool) bondedRatio() sdk.Rat { + if p.TotalSupply > 0 { + return sdk.NewRat(p.BondedPool, p.TotalSupply) + } + return sdk.ZeroRat +} + +// get the exchange rate of bonded token per issued share +func (p Pool) bondedShareExRate() sdk.Rat { + if p.BondedShares.IsZero() { + return sdk.OneRat + } + return sdk.NewRat(p.BondedPool).Quo(p.BondedShares) +} + +// get the exchange rate of unbonded tokens held in candidates per issued share +func (p Pool) unbondedShareExRate() sdk.Rat { + if p.UnbondedShares.IsZero() { + return sdk.OneRat + } + return sdk.NewRat(p.UnbondedPool).Quo(p.UnbondedShares) +} + +//_______________________________________________________________________________________________________ + +// CandidateStatus - status of a validator-candidate +type CandidateStatus byte + +const ( + // nolint + Bonded CandidateStatus = 0x00 + Unbonded CandidateStatus = 0x01 + Revoked CandidateStatus = 0x02 +) + +// Candidate defines the total amount of bond shares and their exchange rate to +// coins. Accumulation of interest is modelled as an in increase in the +// exchange rate, and slashing as a decrease. When coins are delegated to this +// candidate, the candidate is credited with a DelegatorBond whose number of +// bond shares is based on the amount of coins delegated divided by the current +// exchange rate. Voting power can be calculated as total bonds multiplied by +// exchange rate. +type Candidate struct { + Status CandidateStatus `json:"status"` // Bonded status + Address sdk.Address `json:"owner"` // Sender of BondTx - UnbondTx returns here + PubKey crypto.PubKey `json:"pub_key"` // Pubkey of candidate + Assets sdk.Rat `json:"assets"` // total shares of a global hold pools + Liabilities sdk.Rat `json:"liabilities"` // total shares issued to a candidate's delegators + Description Description `json:"description"` // Description terms for the candidate +} + +// NewCandidate - initialize a new candidate +func NewCandidate(address sdk.Address, pubKey crypto.PubKey, description Description) Candidate { + return Candidate{ + Status: Unbonded, + Address: address, + PubKey: pubKey, + Assets: sdk.ZeroRat, + Liabilities: sdk.ZeroRat, + Description: description, + } +} + +// Description - description fields for a candidate +type Description struct { + Moniker string `json:"moniker"` + Identity string `json:"identity"` + Website string `json:"website"` + Details string `json:"details"` +} + +func NewDescription(moniker, identity, website, details string) Description { + return Description{ + Moniker: moniker, + Identity: identity, + Website: website, + Details: details, + } +} + +// get the exchange rate of global pool shares over delegator shares +func (c Candidate) delegatorShareExRate() sdk.Rat { + if c.Liabilities.IsZero() { + return sdk.OneRat + } + return c.Assets.Quo(c.Liabilities) +} + +// Validator returns a copy of the Candidate as a Validator. +// Should only be called when the Candidate qualifies as a validator. +func (c Candidate) validator() Validator { + return Validator{ + Address: c.Address, // XXX !!! + VotingPower: c.Assets, + } +} + +//XXX updateDescription function +//XXX enforce limit to number of description characters + +//______________________________________________________________________ + +// Validator is one of the top Candidates +type Validator struct { + Address sdk.Address `json:"address"` // Address of validator + VotingPower sdk.Rat `json:"voting_power"` // Voting power if considered a validator +} + +// ABCIValidator - Get the validator from a bond value +/* TODO +func (v Validator) ABCIValidator() (*abci.Validator, error) { + pkBytes, err := wire.MarshalBinary(v.PubKey) + if err != nil { + return nil, err + } + return &abci.Validator{ + PubKey: pkBytes, + Power: v.VotingPower.Evaluate(), + }, nil +} +*/ + +//_________________________________________________________________________ + +// Candidates - list of Candidates +type Candidates []Candidate + +//_________________________________________________________________________ + +// DelegatorBond represents the bond with tokens held by an account. It is +// owned by one delegator, and is associated with the voting power of one +// pubKey. +// TODO better way of managing space +type DelegatorBond struct { + DelegatorAddr sdk.Address `json:"delegatoraddr"` + CandidateAddr sdk.Address `json:"candidate_addr"` + Shares sdk.Rat `json:"shares"` +} diff --git a/x/stake/types_test.go b/x/stake/types_test.go new file mode 100644 index 000000000..ec16f32d9 --- /dev/null +++ b/x/stake/types_test.go @@ -0,0 +1,3 @@ +package stake + +// XXX test global state functions, candidate exchange rate functions etc. diff --git a/x/stake/wire.go b/x/stake/wire.go new file mode 100644 index 000000000..4516f89f2 --- /dev/null +++ b/x/stake/wire.go @@ -0,0 +1,12 @@ +package stake + +import ( + "github.com/cosmos/cosmos-sdk/wire" +) + +// XXX complete +func RegisterWire(cdc *wire.Codec) { + // TODO include option to always include prefix bytes. + //cdc.RegisterConcrete(SendMsg{}, "cosmos-sdk/SendMsg", nil) + //cdc.RegisterConcrete(IssueMsg{}, "cosmos-sdk/IssueMsg", nil) +} diff --git a/x/staking/handler.go b/x/staking/handler.go deleted file mode 100644 index c14756b7a..000000000 --- a/x/staking/handler.go +++ /dev/null @@ -1,69 +0,0 @@ -package staking - -import ( - abci "github.com/tendermint/abci/types" - - sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/cosmos/cosmos-sdk/x/bank" -) - -func NewHandler(sm StakingMapper, ck bank.CoinKeeper) sdk.Handler { - return func(ctx sdk.Context, msg sdk.Msg) sdk.Result { - switch msg := msg.(type) { - case BondMsg: - return handleBondMsg(ctx, sm, ck, msg) - case UnbondMsg: - return handleUnbondMsg(ctx, sm, ck, msg) - default: - return sdk.ErrUnknownRequest("No match for message type.").Result() - } - } -} - -func handleBondMsg(ctx sdk.Context, sm StakingMapper, ck bank.CoinKeeper, msg BondMsg) sdk.Result { - _, err := ck.SubtractCoins(ctx, msg.Address, []sdk.Coin{msg.Stake}) - if err != nil { - return err.Result() - } - - power, err := sm.Bond(ctx, msg.Address, msg.PubKey, msg.Stake.Amount) - if err != nil { - return err.Result() - } - - valSet := abci.Validator{ - PubKey: msg.PubKey.Bytes(), - Power: power, - } - - return sdk.Result{ - Code: sdk.CodeOK, - ValidatorUpdates: abci.Validators{valSet}, - } -} - -func handleUnbondMsg(ctx sdk.Context, sm StakingMapper, ck bank.CoinKeeper, msg UnbondMsg) sdk.Result { - pubKey, power, err := sm.Unbond(ctx, msg.Address) - if err != nil { - return err.Result() - } - - stake := sdk.Coin{ - Denom: "mycoin", - Amount: power, - } - _, err = ck.AddCoins(ctx, msg.Address, sdk.Coins{stake}) - if err != nil { - return err.Result() - } - - valSet := abci.Validator{ - PubKey: pubKey.Bytes(), - Power: int64(0), - } - - return sdk.Result{ - Code: sdk.CodeOK, - ValidatorUpdates: abci.Validators{valSet}, - } -} diff --git a/x/staking/mapper.go b/x/staking/mapper.go deleted file mode 100644 index 7f155d00a..000000000 --- a/x/staking/mapper.go +++ /dev/null @@ -1,92 +0,0 @@ -package staking - -import ( - crypto "github.com/tendermint/go-crypto" - - sdk "github.com/cosmos/cosmos-sdk/types" - wire "github.com/cosmos/cosmos-sdk/wire" -) - -type StakingMapper struct { - key sdk.StoreKey - cdc *wire.Codec -} - -func NewMapper(key sdk.StoreKey) StakingMapper { - cdc := wire.NewCodec() - return StakingMapper{ - key: key, - cdc: cdc, - } -} - -func (sm StakingMapper) getBondInfo(ctx sdk.Context, addr sdk.Address) bondInfo { - store := ctx.KVStore(sm.key) - bz := store.Get(addr) - if bz == nil { - return bondInfo{} - } - var bi bondInfo - err := sm.cdc.UnmarshalBinary(bz, &bi) - if err != nil { - panic(err) - } - return bi -} - -func (sm StakingMapper) setBondInfo(ctx sdk.Context, addr sdk.Address, bi bondInfo) { - store := ctx.KVStore(sm.key) - bz, err := sm.cdc.MarshalBinary(bi) - if err != nil { - panic(err) - } - store.Set(addr, bz) -} - -func (sm StakingMapper) deleteBondInfo(ctx sdk.Context, addr sdk.Address) { - store := ctx.KVStore(sm.key) - store.Delete(addr) -} - -func (sm StakingMapper) Bond(ctx sdk.Context, addr sdk.Address, pubKey crypto.PubKey, power int64) (int64, sdk.Error) { - - bi := sm.getBondInfo(ctx, addr) - if bi.isEmpty() { - bi = bondInfo{ - PubKey: pubKey, - Power: power, - } - sm.setBondInfo(ctx, addr, bi) - return bi.Power, nil - } - - newPower := bi.Power + power - newBi := bondInfo{ - PubKey: bi.PubKey, - Power: newPower, - } - sm.setBondInfo(ctx, addr, newBi) - - return newBi.Power, nil -} - -func (sm StakingMapper) Unbond(ctx sdk.Context, addr sdk.Address) (crypto.PubKey, int64, sdk.Error) { - bi := sm.getBondInfo(ctx, addr) - if bi.isEmpty() { - return crypto.PubKey{}, 0, ErrInvalidUnbond() - } - sm.deleteBondInfo(ctx, addr) - return bi.PubKey, bi.Power, nil -} - -type bondInfo struct { - PubKey crypto.PubKey - Power int64 -} - -func (bi bondInfo) isEmpty() bool { - if bi == (bondInfo{}) { - return true - } - return false -}