Merge branch 'develop' into matt/ibc-spec
This commit is contained in:
commit
8231fd4f04
|
@ -13,7 +13,7 @@ docs/_build
|
|||
.DS_Store
|
||||
coverage.txt
|
||||
profile.out
|
||||
|
||||
.vscode
|
||||
|
||||
### Vagrant ###
|
||||
.vagrant/
|
||||
|
|
|
@ -84,6 +84,18 @@
|
|||
packages = ["."]
|
||||
revision = "553a641470496b2327abcac10b36396bd98e45c9"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/gorilla/context"
|
||||
packages = ["."]
|
||||
revision = "1ea25387ff6f684839d82767c1733ff4d4d15d0a"
|
||||
version = "v1.1"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/gorilla/mux"
|
||||
packages = ["."]
|
||||
revision = "53c1911da2b537f792e7cafcb446b05ffe33b996"
|
||||
version = "v1.6.1"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/gorilla/websocket"
|
||||
packages = ["."]
|
||||
|
@ -331,7 +343,7 @@
|
|||
revision = "cd2ba4aa7f95e16fe99570260ad58415e7ad4660"
|
||||
|
||||
[[projects]]
|
||||
branch = "develop"
|
||||
branch = "rigel/cli-refactor"
|
||||
name = "github.com/tendermint/tmlibs"
|
||||
packages = [
|
||||
"autofile",
|
||||
|
|
|
@ -82,7 +82,7 @@
|
|||
name = "github.com/tendermint/tendermint"
|
||||
|
||||
[[override]]
|
||||
branch = "develop"
|
||||
branch = "rigel/cli-refactor"
|
||||
name = "github.com/tendermint/tmlibs"
|
||||
|
||||
[prune]
|
||||
|
|
|
@ -371,6 +371,13 @@ func (app *BaseApp) runTx(isCheckTx bool, txBytes []byte, tx sdk.Tx) (result sdk
|
|||
}
|
||||
}
|
||||
|
||||
// Match route.
|
||||
msgType := msg.Type()
|
||||
handler := app.router.Route(msgType)
|
||||
if handler == nil {
|
||||
return sdk.ErrUnknownRequest("Unrecognized Msg type: " + msgType).Result()
|
||||
}
|
||||
|
||||
// Get the correct cache
|
||||
var msCache sdk.CacheMultiStore
|
||||
if isCheckTx == true {
|
||||
|
@ -384,9 +391,6 @@ func (app *BaseApp) runTx(isCheckTx bool, txBytes []byte, tx sdk.Tx) (result sdk
|
|||
|
||||
}
|
||||
|
||||
// Match and run route.
|
||||
msgType := msg.Type()
|
||||
handler := app.router.Route(msgType)
|
||||
result = handler(ctx, msg)
|
||||
|
||||
// If result was successful, write to app.checkState.ms or app.deliverState.ms
|
||||
|
|
|
@ -328,7 +328,6 @@ func (tx testUpdatePowerTx) GetMsg() sdk.Msg { return tx
|
|||
func (tx testUpdatePowerTx) GetSignBytes() []byte { return nil }
|
||||
func (tx testUpdatePowerTx) ValidateBasic() sdk.Error { return nil }
|
||||
func (tx testUpdatePowerTx) GetSigners() []sdk.Address { return nil }
|
||||
func (tx testUpdatePowerTx) GetFeePayer() sdk.Address { return nil }
|
||||
func (tx testUpdatePowerTx) GetSignatures() []sdk.StdSignature { return nil }
|
||||
|
||||
func TestValidatorChange(t *testing.T) {
|
||||
|
|
|
@ -88,22 +88,25 @@ func GetFromAddress() (from sdk.Address, err error) {
|
|||
}
|
||||
|
||||
// sign and build the transaction from the msg
|
||||
func SignAndBuild(msg sdk.Msg, cdc *wire.Codec) ([]byte, error) {
|
||||
func SignAndBuild(name, passphrase string, msg sdk.Msg, cdc *wire.Codec) ([]byte, error) {
|
||||
|
||||
// build the Sign Messsage from the Standard Message
|
||||
chainID := viper.GetString(client.FlagChainID)
|
||||
sequence := int64(viper.GetInt(client.FlagSequence))
|
||||
signMsg := sdk.StdSignMsg{
|
||||
ChainID: chainID,
|
||||
Sequences: []int64{sequence},
|
||||
Msg: msg,
|
||||
}
|
||||
|
||||
keybase, err := keys.GetKeyBase()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
name := viper.GetString(client.FlagName)
|
||||
|
||||
// sign and build
|
||||
bz := msg.GetSignBytes()
|
||||
buf := client.BufferStdin()
|
||||
prompt := fmt.Sprintf("Password to sign with '%s':", name)
|
||||
passphrase, err := client.GetPassword(prompt, buf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bz := signMsg.Bytes()
|
||||
|
||||
sig, pubkey, err := keybase.Sign(name, passphrase, bz)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -115,14 +118,14 @@ func SignAndBuild(msg sdk.Msg, cdc *wire.Codec) ([]byte, error) {
|
|||
}}
|
||||
|
||||
// marshal bytes
|
||||
tx := sdk.NewStdTx(msg, sigs)
|
||||
tx := sdk.NewStdTx(signMsg.Msg, signMsg.Fee, sigs)
|
||||
|
||||
return cdc.MarshalBinary(tx)
|
||||
}
|
||||
|
||||
// sign and build the transaction from the msg
|
||||
func SignBuildBroadcast(msg sdk.Msg, cdc *wire.Codec) (*ctypes.ResultBroadcastTxCommit, error) {
|
||||
txBytes, err := SignAndBuild(msg, cdc)
|
||||
func SignBuildBroadcast(name string, passphrase string, msg sdk.Msg, cdc *wire.Codec) (*ctypes.ResultBroadcastTxCommit, error) {
|
||||
txBytes, err := SignAndBuild(name, passphrase, msg, cdc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
@ -6,7 +6,8 @@ import (
|
|||
dbm "github.com/tendermint/tmlibs/db"
|
||||
)
|
||||
|
||||
// GetKeyBase initializes a keybase based on the configuration
|
||||
// GetKeyBase initializes a keybase based on the given db.
|
||||
// The KeyBase manages all activity requiring access to a key.
|
||||
func GetKeyBase(db dbm.DB) keys.Keybase {
|
||||
keybase := keys.New(
|
||||
db,
|
||||
|
|
|
@ -1,9 +1,13 @@
|
|||
package keys
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/client"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
|
@ -120,3 +124,89 @@ func printCreate(info keys.Info, seed string) {
|
|||
panic(fmt.Sprintf("I can't speak: %s", output))
|
||||
}
|
||||
}
|
||||
|
||||
// REST
|
||||
|
||||
type NewKeyBody struct {
|
||||
Name string `json:"name"`
|
||||
Password string `json:"password"`
|
||||
Seed string `json:"seed"`
|
||||
}
|
||||
|
||||
func AddNewKeyRequestHandler(w http.ResponseWriter, r *http.Request) {
|
||||
var kb keys.Keybase
|
||||
var m NewKeyBody
|
||||
|
||||
kb, err := GetKeyBase()
|
||||
if err != nil {
|
||||
w.WriteHeader(500)
|
||||
w.Write([]byte(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
body, err := ioutil.ReadAll(r.Body)
|
||||
err = json.Unmarshal(body, &m)
|
||||
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
w.Write([]byte(err.Error()))
|
||||
return
|
||||
}
|
||||
if m.Name == "" {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
w.Write([]byte("You have to specify a name for the locally stored account."))
|
||||
return
|
||||
}
|
||||
if m.Password == "" {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
w.Write([]byte("You have to specify a password for the locally stored account."))
|
||||
return
|
||||
}
|
||||
if m.Seed == "" {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
w.Write([]byte("You have to specify a seed for the locally stored account."))
|
||||
return
|
||||
}
|
||||
|
||||
// check if already exists
|
||||
infos, err := kb.List()
|
||||
for _, i := range infos {
|
||||
if i.Name == m.Name {
|
||||
w.WriteHeader(http.StatusConflict)
|
||||
w.Write([]byte(fmt.Sprintf("Account with name %s already exists.", m.Name)))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// create account
|
||||
info, err := kb.Recover(m.Name, m.Password, m.Seed)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Write([]byte(err.Error()))
|
||||
return
|
||||
}
|
||||
w.Write([]byte(info.PubKey.Address().String()))
|
||||
}
|
||||
|
||||
// function to just a new seed to display in the UI before actually persisting it in the keybase
|
||||
func getSeed(algo keys.CryptoAlgo) string {
|
||||
kb := client.MockKeyBase()
|
||||
pass := "throwing-this-key-away"
|
||||
name := "inmemorykey"
|
||||
|
||||
_, seed, _ := kb.Create(name, pass, algo)
|
||||
return seed
|
||||
}
|
||||
|
||||
func SeedRequestHandler(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
algoType := vars["type"]
|
||||
// algo type defaults to ed25519
|
||||
if algoType == "" {
|
||||
algoType = "ed25519"
|
||||
}
|
||||
algo := keys.CryptoAlgo(algoType)
|
||||
|
||||
seed := getSeed(algo)
|
||||
w.Write([]byte(seed))
|
||||
}
|
||||
|
|
|
@ -1,10 +1,14 @@
|
|||
package keys
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/client"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/pkg/errors"
|
||||
keys "github.com/tendermint/go-crypto/keys"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
@ -43,3 +47,41 @@ func runDeleteCmd(cmd *cobra.Command, args []string) error {
|
|||
fmt.Println("Password deleted forever (uh oh!)")
|
||||
return nil
|
||||
}
|
||||
|
||||
// REST
|
||||
|
||||
type DeleteKeyBody struct {
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
func DeleteKeyRequestHandler(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
name := vars["name"]
|
||||
var kb keys.Keybase
|
||||
var m DeleteKeyBody
|
||||
|
||||
decoder := json.NewDecoder(r.Body)
|
||||
err := decoder.Decode(&m)
|
||||
if err != nil {
|
||||
w.WriteHeader(400)
|
||||
w.Write([]byte(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
kb, err = GetKeyBase()
|
||||
if err != nil {
|
||||
w.WriteHeader(500)
|
||||
w.Write([]byte(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
// TODO handle error if key is not available or pass is wrong
|
||||
err = kb.Delete(name, m.Password)
|
||||
if err != nil {
|
||||
w.WriteHeader(500)
|
||||
w.Write([]byte(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(200)
|
||||
}
|
||||
|
|
|
@ -1,6 +1,13 @@
|
|||
package keys
|
||||
|
||||
import "github.com/spf13/cobra"
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// CMD
|
||||
|
||||
// listKeysCmd represents the list command
|
||||
var listKeysCmd = &cobra.Command{
|
||||
|
@ -23,3 +30,36 @@ func runListCmd(cmd *cobra.Command, args []string) error {
|
|||
}
|
||||
return err
|
||||
}
|
||||
|
||||
//REST
|
||||
|
||||
func QueryKeysRequestHandler(w http.ResponseWriter, r *http.Request) {
|
||||
kb, err := GetKeyBase()
|
||||
if err != nil {
|
||||
w.WriteHeader(500)
|
||||
w.Write([]byte(err.Error()))
|
||||
return
|
||||
}
|
||||
infos, err := kb.List()
|
||||
if err != nil {
|
||||
w.WriteHeader(500)
|
||||
w.Write([]byte(err.Error()))
|
||||
return
|
||||
}
|
||||
// an empty list will be JSONized as null, but we want to keep the empty list
|
||||
if len(infos) == 0 {
|
||||
w.Write([]byte("[]"))
|
||||
return
|
||||
}
|
||||
keysOutput := make([]KeyOutput, len(infos))
|
||||
for i, info := range infos {
|
||||
keysOutput[i] = KeyOutput{Name: info.Name, Address: info.PubKey.Address().String()}
|
||||
}
|
||||
output, err := json.MarshalIndent(keysOutput, "", " ")
|
||||
if err != nil {
|
||||
w.WriteHeader(500)
|
||||
w.Write([]byte(err.Error()))
|
||||
return
|
||||
}
|
||||
w.Write(output)
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ package keys
|
|||
|
||||
import (
|
||||
"github.com/cosmos/cosmos-sdk/client"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
|
@ -27,3 +28,12 @@ func Commands() *cobra.Command {
|
|||
)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func RegisterRoutes(r *mux.Router) {
|
||||
r.HandleFunc("/keys", QueryKeysRequestHandler).Methods("GET")
|
||||
r.HandleFunc("/keys", AddNewKeyRequestHandler).Methods("POST")
|
||||
r.HandleFunc("/keys/seed", SeedRequestHandler).Methods("GET")
|
||||
r.HandleFunc("/keys/{name}", GetKeyRequestHandler).Methods("GET")
|
||||
r.HandleFunc("/keys/{name}", UpdateKeyRequestHandler).Methods("PUT")
|
||||
r.HandleFunc("/keys/{name}", DeleteKeyRequestHandler).Methods("DELETE")
|
||||
}
|
||||
|
|
|
@ -1,7 +1,12 @@
|
|||
package keys
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/pkg/errors"
|
||||
keys "github.com/tendermint/go-crypto/keys"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
@ -13,20 +18,51 @@ var showKeysCmd = &cobra.Command{
|
|||
RunE: runShowCmd,
|
||||
}
|
||||
|
||||
func getKey(name string) (keys.Info, error) {
|
||||
kb, err := GetKeyBase()
|
||||
if err != nil {
|
||||
return keys.Info{}, err
|
||||
}
|
||||
|
||||
return kb.Get(name)
|
||||
}
|
||||
|
||||
// CMD
|
||||
|
||||
func runShowCmd(cmd *cobra.Command, args []string) error {
|
||||
if len(args) != 1 || len(args[0]) == 0 {
|
||||
return errors.New("You must provide a name for the key")
|
||||
}
|
||||
name := args[0]
|
||||
|
||||
kb, err := GetKeyBase()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
info, err := kb.Get(name)
|
||||
info, err := getKey(name)
|
||||
if err == nil {
|
||||
printInfo(info)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// REST
|
||||
|
||||
func GetKeyRequestHandler(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
name := vars["name"]
|
||||
|
||||
info, err := getKey(name)
|
||||
// TODO check for the error if key actually does not exist, instead of assuming this as the reason
|
||||
if err != nil {
|
||||
w.WriteHeader(404)
|
||||
w.Write([]byte(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
keyOutput := KeyOutput{Name: info.Name, Address: info.PubKey.Address().String()}
|
||||
output, err := json.MarshalIndent(keyOutput, "", " ")
|
||||
if err != nil {
|
||||
w.WriteHeader(500)
|
||||
w.Write([]byte(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
w.Write(output)
|
||||
}
|
||||
|
|
|
@ -1,10 +1,14 @@
|
|||
package keys
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/client"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/pkg/errors"
|
||||
keys "github.com/tendermint/go-crypto/keys"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
@ -48,3 +52,42 @@ func runUpdateCmd(cmd *cobra.Command, args []string) error {
|
|||
fmt.Println("Password successfully updated!")
|
||||
return nil
|
||||
}
|
||||
|
||||
// REST
|
||||
|
||||
type UpdateKeyBody struct {
|
||||
NewPassword string `json:"new_password"`
|
||||
OldPassword string `json:"old_password"`
|
||||
}
|
||||
|
||||
func UpdateKeyRequestHandler(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
name := vars["name"]
|
||||
var kb keys.Keybase
|
||||
var m UpdateKeyBody
|
||||
|
||||
decoder := json.NewDecoder(r.Body)
|
||||
err := decoder.Decode(&m)
|
||||
if err != nil {
|
||||
w.WriteHeader(400)
|
||||
w.Write([]byte(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
kb, err = GetKeyBase()
|
||||
if err != nil {
|
||||
w.WriteHeader(500)
|
||||
w.Write([]byte(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
// TODO check if account exists and if password is correct
|
||||
err = kb.Update(name, m.OldPassword, m.NewPassword)
|
||||
if err != nil {
|
||||
w.WriteHeader(401)
|
||||
w.Write([]byte(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(200)
|
||||
}
|
||||
|
|
|
@ -20,6 +20,14 @@ var (
|
|||
keybase keys.Keybase
|
||||
)
|
||||
|
||||
// used for outputting keys.Info over REST
|
||||
type KeyOutput struct {
|
||||
Name string `json:"name"`
|
||||
Address string `json:"address"`
|
||||
// TODO add pubkey?
|
||||
// Pubkey string `json:"pubkey"`
|
||||
}
|
||||
|
||||
// GetKeyBase initializes a keybase based on the configuration
|
||||
func GetKeyBase() (keys.Keybase, error) {
|
||||
if keybase == nil {
|
||||
|
@ -33,6 +41,11 @@ func GetKeyBase() (keys.Keybase, error) {
|
|||
return keybase, nil
|
||||
}
|
||||
|
||||
// used to set the keybase manually in test
|
||||
func SetKeyBase(kb keys.Keybase) {
|
||||
keybase = kb
|
||||
}
|
||||
|
||||
func printInfo(info keys.Info) {
|
||||
switch viper.Get(cli.OutputFlag) {
|
||||
case "text":
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
tmp-base*
|
|
@ -0,0 +1,69 @@
|
|||
package lcd
|
||||
|
||||
// NOTE: COPIED VERBATIM FROM tendermint/tendermint/rpc/test/helpers.go
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
cmn "github.com/tendermint/tmlibs/common"
|
||||
|
||||
cfg "github.com/tendermint/tendermint/config"
|
||||
ctypes "github.com/tendermint/tendermint/rpc/core/types"
|
||||
rpcclient "github.com/tendermint/tendermint/rpc/lib/client"
|
||||
)
|
||||
|
||||
var globalConfig *cfg.Config
|
||||
|
||||
func waitForRPC() {
|
||||
laddr := GetConfig().RPC.ListenAddress
|
||||
fmt.Println("LADDR", laddr)
|
||||
client := rpcclient.NewJSONRPCClient(laddr)
|
||||
result := new(ctypes.ResultStatus)
|
||||
for {
|
||||
_, err := client.Call("status", map[string]interface{}{}, result)
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// f**ing long, but unique for each test
|
||||
func makePathname() string {
|
||||
// get path
|
||||
p, err := os.Getwd()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
// fmt.Println(p)
|
||||
sep := string(filepath.Separator)
|
||||
return strings.Replace(p, sep, "_", -1)
|
||||
}
|
||||
|
||||
func randPort() int {
|
||||
return int(cmn.RandUint16()/2 + 10000)
|
||||
}
|
||||
|
||||
func makeAddrs() (string, string, string) {
|
||||
start := randPort()
|
||||
return fmt.Sprintf("tcp://0.0.0.0:%d", start),
|
||||
fmt.Sprintf("tcp://0.0.0.0:%d", start+1),
|
||||
fmt.Sprintf("tcp://0.0.0.0:%d", start+2)
|
||||
}
|
||||
|
||||
// GetConfig returns a config for the test cases as a singleton
|
||||
func GetConfig() *cfg.Config {
|
||||
if globalConfig == nil {
|
||||
pathname := makePathname()
|
||||
globalConfig = cfg.ResetTestRoot(pathname)
|
||||
|
||||
// and we use random ports to run in parallel
|
||||
tm, rpc, _ := makeAddrs()
|
||||
globalConfig.P2P.ListenAddress = tm
|
||||
globalConfig.RPC.ListenAddress = rpc
|
||||
globalConfig.TxIndex.IndexTags = "app.creator" // see kvstore application
|
||||
}
|
||||
return globalConfig
|
||||
}
|
|
@ -0,0 +1,443 @@
|
|||
package lcd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"regexp"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
abci "github.com/tendermint/abci/types"
|
||||
cryptoKeys "github.com/tendermint/go-crypto/keys"
|
||||
tmcfg "github.com/tendermint/tendermint/config"
|
||||
nm "github.com/tendermint/tendermint/node"
|
||||
p2p "github.com/tendermint/tendermint/p2p"
|
||||
"github.com/tendermint/tendermint/proxy"
|
||||
ctypes "github.com/tendermint/tendermint/rpc/core/types"
|
||||
tmrpc "github.com/tendermint/tendermint/rpc/lib/server"
|
||||
tmtypes "github.com/tendermint/tendermint/types"
|
||||
dbm "github.com/tendermint/tmlibs/db"
|
||||
"github.com/tendermint/tmlibs/log"
|
||||
|
||||
client "github.com/cosmos/cosmos-sdk/client"
|
||||
keys "github.com/cosmos/cosmos-sdk/client/keys"
|
||||
bapp "github.com/cosmos/cosmos-sdk/examples/basecoin/app"
|
||||
btypes "github.com/cosmos/cosmos-sdk/examples/basecoin/types"
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
"github.com/cosmos/cosmos-sdk/wire"
|
||||
"github.com/cosmos/cosmos-sdk/x/auth"
|
||||
)
|
||||
|
||||
var (
|
||||
coinDenom = "mycoin"
|
||||
coinAmount = int64(10000000)
|
||||
|
||||
// XXX bad globals
|
||||
port string // XXX: but it's the int ...
|
||||
name string = "test"
|
||||
password string = "0123456789"
|
||||
seed string
|
||||
sendAddr string
|
||||
)
|
||||
|
||||
func TestKeys(t *testing.T) {
|
||||
|
||||
// empty keys
|
||||
// XXX: the test comes with a key setup
|
||||
/*
|
||||
res, body := request(t, port, "GET", "/keys", nil)
|
||||
require.Equal(t, http.StatusOK, res.StatusCode, body)
|
||||
assert.Equal(t, "[]", body, "Expected an empty array")
|
||||
*/
|
||||
|
||||
// get seed
|
||||
res, body := request(t, port, "GET", "/keys/seed", nil)
|
||||
require.Equal(t, http.StatusOK, res.StatusCode, body)
|
||||
newSeed := body
|
||||
reg, err := regexp.Compile(`([a-z]+ ){12}`)
|
||||
require.Nil(t, err)
|
||||
match := reg.MatchString(seed)
|
||||
assert.True(t, match, "Returned seed has wrong foramt", seed)
|
||||
|
||||
newName := "test_newname"
|
||||
newPassword := "0987654321"
|
||||
|
||||
// add key
|
||||
var jsonStr = []byte(fmt.Sprintf(`{"name":"test_fail", "password":"%s"}`, password))
|
||||
res, body = request(t, port, "POST", "/keys", jsonStr)
|
||||
|
||||
assert.Equal(t, http.StatusBadRequest, res.StatusCode, "Account creation should require a seed")
|
||||
|
||||
jsonStr = []byte(fmt.Sprintf(`{"name":"%s", "password":"%s", "seed": "%s"}`, newName, newPassword, newSeed))
|
||||
res, body = request(t, port, "POST", "/keys", jsonStr)
|
||||
|
||||
assert.Equal(t, http.StatusOK, res.StatusCode, body)
|
||||
addr := body
|
||||
assert.Len(t, addr, 40, "Returned address has wrong format", addr)
|
||||
|
||||
// existing keys
|
||||
res, body = request(t, port, "GET", "/keys", nil)
|
||||
require.Equal(t, http.StatusOK, res.StatusCode, body)
|
||||
var m [2]keys.KeyOutput
|
||||
err = json.Unmarshal([]byte(body), &m)
|
||||
require.Nil(t, err)
|
||||
|
||||
assert.Equal(t, m[0].Name, name, "Did not serve keys name correctly")
|
||||
assert.Equal(t, m[0].Address, sendAddr, "Did not serve keys Address correctly")
|
||||
assert.Equal(t, m[1].Name, newName, "Did not serve keys name correctly")
|
||||
assert.Equal(t, m[1].Address, addr, "Did not serve keys Address correctly")
|
||||
|
||||
// select key
|
||||
keyEndpoint := fmt.Sprintf("/keys/%s", newName)
|
||||
res, body = request(t, port, "GET", keyEndpoint, nil)
|
||||
require.Equal(t, http.StatusOK, res.StatusCode, body)
|
||||
var m2 keys.KeyOutput
|
||||
err = json.Unmarshal([]byte(body), &m2)
|
||||
require.Nil(t, err)
|
||||
|
||||
assert.Equal(t, newName, m2.Name, "Did not serve keys name correctly")
|
||||
assert.Equal(t, addr, m2.Address, "Did not serve keys Address correctly")
|
||||
|
||||
// update key
|
||||
jsonStr = []byte(fmt.Sprintf(`{"old_password":"%s", "new_password":"12345678901"}`, newPassword))
|
||||
res, body = request(t, port, "PUT", keyEndpoint, jsonStr)
|
||||
require.Equal(t, http.StatusOK, res.StatusCode, body)
|
||||
|
||||
// here it should say unauthorized as we changed the password before
|
||||
res, body = request(t, port, "PUT", keyEndpoint, jsonStr)
|
||||
require.Equal(t, http.StatusUnauthorized, res.StatusCode, body)
|
||||
|
||||
// delete key
|
||||
jsonStr = []byte(`{"password":"12345678901"}`)
|
||||
res, body = request(t, port, "DELETE", keyEndpoint, jsonStr)
|
||||
require.Equal(t, http.StatusOK, res.StatusCode, body)
|
||||
}
|
||||
|
||||
func TestVersion(t *testing.T) {
|
||||
|
||||
// node info
|
||||
res, body := request(t, port, "GET", "/version", nil)
|
||||
require.Equal(t, http.StatusOK, res.StatusCode, body)
|
||||
|
||||
reg, err := regexp.Compile(`\d+\.\d+\.\d+(-dev)?`)
|
||||
require.Nil(t, err)
|
||||
match := reg.MatchString(body)
|
||||
assert.True(t, match, body)
|
||||
}
|
||||
|
||||
func TestNodeStatus(t *testing.T) {
|
||||
|
||||
// node info
|
||||
res, body := request(t, port, "GET", "/node_info", nil)
|
||||
require.Equal(t, http.StatusOK, res.StatusCode, body)
|
||||
|
||||
var nodeInfo p2p.NodeInfo
|
||||
err := json.Unmarshal([]byte(body), &nodeInfo)
|
||||
require.Nil(t, err, "Couldn't parse node info")
|
||||
|
||||
assert.NotEqual(t, p2p.NodeInfo{}, nodeInfo, "res: %v", res)
|
||||
|
||||
// syncing
|
||||
res, body = request(t, port, "GET", "/syncing", nil)
|
||||
require.Equal(t, http.StatusOK, res.StatusCode, body)
|
||||
|
||||
// we expect that there is no other node running so the syncing state is "false"
|
||||
// we c
|
||||
assert.Equal(t, "false", body)
|
||||
}
|
||||
|
||||
func TestBlock(t *testing.T) {
|
||||
|
||||
time.Sleep(time.Second * 2) // TODO: LOL -> wait for blocks
|
||||
|
||||
var resultBlock ctypes.ResultBlock
|
||||
|
||||
res, body := request(t, port, "GET", "/blocks/latest", nil)
|
||||
require.Equal(t, http.StatusOK, res.StatusCode, body)
|
||||
|
||||
err := json.Unmarshal([]byte(body), &resultBlock)
|
||||
require.Nil(t, err, "Couldn't parse block")
|
||||
|
||||
assert.NotEqual(t, ctypes.ResultBlock{}, resultBlock)
|
||||
|
||||
// --
|
||||
|
||||
res, body = request(t, port, "GET", "/blocks/1", nil)
|
||||
require.Equal(t, http.StatusOK, res.StatusCode, body)
|
||||
|
||||
err = json.Unmarshal([]byte(body), &resultBlock)
|
||||
require.Nil(t, err, "Couldn't parse block")
|
||||
|
||||
assert.NotEqual(t, ctypes.ResultBlock{}, resultBlock)
|
||||
|
||||
// --
|
||||
|
||||
res, body = request(t, port, "GET", "/blocks/1000000000", nil)
|
||||
require.Equal(t, http.StatusNotFound, res.StatusCode, body)
|
||||
}
|
||||
|
||||
func TestValidators(t *testing.T) {
|
||||
|
||||
var resultVals ctypes.ResultValidators
|
||||
|
||||
res, body := request(t, port, "GET", "/validatorsets/latest", nil)
|
||||
require.Equal(t, http.StatusOK, res.StatusCode, body)
|
||||
|
||||
err := json.Unmarshal([]byte(body), &resultVals)
|
||||
require.Nil(t, err, "Couldn't parse validatorset")
|
||||
|
||||
assert.NotEqual(t, ctypes.ResultValidators{}, resultVals)
|
||||
|
||||
// --
|
||||
|
||||
res, body = request(t, port, "GET", "/validatorsets/1", nil)
|
||||
require.Equal(t, http.StatusOK, res.StatusCode, body)
|
||||
|
||||
err = json.Unmarshal([]byte(body), &resultVals)
|
||||
require.Nil(t, err, "Couldn't parse validatorset")
|
||||
|
||||
assert.NotEqual(t, ctypes.ResultValidators{}, resultVals)
|
||||
|
||||
// --
|
||||
|
||||
res, body = request(t, port, "GET", "/validatorsets/1000000000", nil)
|
||||
require.Equal(t, http.StatusNotFound, res.StatusCode)
|
||||
}
|
||||
|
||||
func TestCoinSend(t *testing.T) {
|
||||
|
||||
// query empty
|
||||
res, body := request(t, port, "GET", "/accounts/8FA6AB57AD6870F6B5B2E57735F38F2F30E73CB6", nil)
|
||||
require.Equal(t, http.StatusNoContent, res.StatusCode, body)
|
||||
|
||||
// create TX
|
||||
receiveAddr, resultTx := doSend(t, port, seed)
|
||||
|
||||
time.Sleep(time.Second * 2) // T
|
||||
|
||||
// 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-1, mycoins.Amount)
|
||||
|
||||
// query receiver
|
||||
res, body = request(t, port, "GET", "/accounts/"+receiveAddr, nil)
|
||||
require.Equal(t, http.StatusOK, res.StatusCode, body)
|
||||
|
||||
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, int64(1), mycoins.Amount)
|
||||
}
|
||||
|
||||
func TestTxs(t *testing.T) {
|
||||
|
||||
// TODO: re-enable once we can get txs by tag
|
||||
|
||||
// query wrong
|
||||
// res, body := request(t, port, "GET", "/txs", nil)
|
||||
// require.Equal(t, http.StatusBadRequest, res.StatusCode, body)
|
||||
|
||||
// query empty
|
||||
// res, body = request(t, port, "GET", fmt.Sprintf("/txs?tag=coin.sender='%s'", "8FA6AB57AD6870F6B5B2E57735F38F2F30E73CB6"), nil)
|
||||
// require.Equal(t, http.StatusOK, res.StatusCode, body)
|
||||
|
||||
// assert.Equal(t, "[]", body)
|
||||
|
||||
// create TX
|
||||
_, resultTx := doSend(t, port, seed)
|
||||
|
||||
time.Sleep(time.Second * 2) // TO
|
||||
|
||||
// check if tx is findable
|
||||
res, body := request(t, port, "GET", fmt.Sprintf("/txs/%s", resultTx.Hash), nil)
|
||||
require.Equal(t, http.StatusOK, res.StatusCode, body)
|
||||
|
||||
// // query sender
|
||||
// res, body = request(t, port, "GET", fmt.Sprintf("/txs?tag=coin.sender='%s'", addr), nil)
|
||||
// require.Equal(t, http.StatusOK, res.StatusCode, body)
|
||||
|
||||
// assert.NotEqual(t, "[]", body)
|
||||
|
||||
// // query receiver
|
||||
// res, body = request(t, port, "GET", fmt.Sprintf("/txs?tag=coin.receiver='%s'", receiveAddr), nil)
|
||||
// require.Equal(t, http.StatusOK, res.StatusCode, body)
|
||||
|
||||
// assert.NotEqual(t, "[]", body)
|
||||
}
|
||||
|
||||
//__________________________________________________________
|
||||
// helpers
|
||||
|
||||
// strt TM and the LCD in process, listening on their respective sockets
|
||||
func startTMAndLCD() (*nm.Node, net.Listener, error) {
|
||||
|
||||
kb, err := keys.GetKeyBase() // dbm.NewMemDB()) // :(
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
var info cryptoKeys.Info
|
||||
info, seed, err = kb.Create(name, password, cryptoKeys.AlgoEd25519) // XXX global seed
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
pubKey := info.PubKey
|
||||
sendAddr = pubKey.Address().String() // XXX global
|
||||
|
||||
config := GetConfig()
|
||||
config.Consensus.TimeoutCommit = 1000
|
||||
config.Consensus.SkipTimeoutCommit = false
|
||||
|
||||
logger := log.NewTMLogger(log.NewSyncWriter(os.Stdout))
|
||||
logger = log.NewFilter(logger, log.AllowError())
|
||||
privValidatorFile := config.PrivValidatorFile()
|
||||
privVal := tmtypes.LoadOrGenPrivValidatorFS(privValidatorFile)
|
||||
app := bapp.NewBasecoinApp(logger, dbm.NewMemDB())
|
||||
|
||||
genesisFile := config.GenesisFile()
|
||||
genDoc, err := tmtypes.GenesisDocFromFile(genesisFile)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
coins := sdk.Coins{{coinDenom, coinAmount}}
|
||||
appState := btypes.GenesisState{
|
||||
Accounts: []*btypes.GenesisAccount{
|
||||
{
|
||||
Name: "tester",
|
||||
Address: pubKey.Address(),
|
||||
Coins: coins,
|
||||
},
|
||||
},
|
||||
}
|
||||
stateBytes, err := json.Marshal(appState)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
genDoc.AppState = stateBytes
|
||||
|
||||
cdc := wire.NewCodec()
|
||||
|
||||
// LCD listen address
|
||||
port = fmt.Sprintf("%d", 17377) // XXX
|
||||
listenAddr := fmt.Sprintf("tcp://localhost:%s", port) // XXX
|
||||
|
||||
// XXX: need to set this so LCD knows the tendermint node address!
|
||||
viper.Set(client.FlagNode, config.RPC.ListenAddress)
|
||||
viper.Set(client.FlagChainID, genDoc.ChainID)
|
||||
|
||||
node, err := startTM(config, logger, genDoc, privVal, app)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
lcd, err := startLCD(cdc, logger, listenAddr)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
time.Sleep(time.Second * 2)
|
||||
|
||||
return node, lcd, nil
|
||||
}
|
||||
|
||||
// Create & start in-process tendermint node with memdb
|
||||
// and in-process abci application.
|
||||
// TODO: need to clean up the WAL dir or enable it to be not persistent
|
||||
func startTM(cfg *tmcfg.Config, logger log.Logger, genDoc *tmtypes.GenesisDoc, privVal tmtypes.PrivValidator, app abci.Application) (*nm.Node, error) {
|
||||
genDocProvider := func() (*tmtypes.GenesisDoc, error) { return genDoc, nil }
|
||||
dbProvider := func(*nm.DBContext) (dbm.DB, error) { return dbm.NewMemDB(), nil }
|
||||
n, err := nm.NewNode(cfg,
|
||||
privVal,
|
||||
proxy.NewLocalClientCreator(app),
|
||||
genDocProvider,
|
||||
dbProvider,
|
||||
logger.With("module", "node"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = n.Start()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// wait for rpc
|
||||
waitForRPC()
|
||||
|
||||
logger.Info("Tendermint running!")
|
||||
return n, err
|
||||
}
|
||||
|
||||
// start the LCD. note this blocks!
|
||||
func startLCD(cdc *wire.Codec, logger log.Logger, listenAddr string) (net.Listener, error) {
|
||||
handler := createHandler(cdc)
|
||||
return tmrpc.StartHTTPServer(listenAddr, handler, logger)
|
||||
}
|
||||
|
||||
func request(t *testing.T, port, method, path string, payload []byte) (*http.Response, string) {
|
||||
var res *http.Response
|
||||
var err error
|
||||
url := fmt.Sprintf("http://localhost:%v%v", port, path)
|
||||
req, err := http.NewRequest(method, url, bytes.NewBuffer(payload))
|
||||
require.Nil(t, err)
|
||||
res, err = http.DefaultClient.Do(req)
|
||||
// res, err = http.Post(url, "application/json", bytes.NewBuffer(payload))
|
||||
require.Nil(t, err)
|
||||
|
||||
output, err := ioutil.ReadAll(res.Body)
|
||||
require.Nil(t, err)
|
||||
|
||||
return res, string(output)
|
||||
}
|
||||
|
||||
func doSend(t *testing.T, port, seed string) (receiveAddr 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)
|
||||
fmt.Println("BODY", body)
|
||||
fmt.Println("ACC", acc)
|
||||
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", "/accounts/"+receiveAddr+"/send", jsonStr)
|
||||
require.Equal(t, http.StatusOK, res.StatusCode, body)
|
||||
|
||||
err = json.Unmarshal([]byte(body), &resultTx)
|
||||
require.Nil(t, err)
|
||||
|
||||
return receiveAddr, resultTx
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
package lcd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
nm "github.com/tendermint/tendermint/node"
|
||||
)
|
||||
|
||||
var node *nm.Node
|
||||
|
||||
// See https://golang.org/pkg/testing/#hdr-Main
|
||||
// for more details
|
||||
func TestMain(m *testing.M) {
|
||||
// start a basecoind node and LCD server in the background to test against
|
||||
|
||||
// run all the tests against a single server instance
|
||||
node, lcd, err := startTMAndLCD()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
code := m.Run()
|
||||
|
||||
// tear down
|
||||
// TODO: cleanup
|
||||
// TODO: it would be great if TM could run without
|
||||
// persiting anything in the first place
|
||||
node.Stop()
|
||||
node.Wait()
|
||||
|
||||
// just a listener ...
|
||||
lcd.Close()
|
||||
|
||||
os.Exit(code)
|
||||
}
|
|
@ -1,36 +1,82 @@
|
|||
package lcd
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
"github.com/tendermint/tmlibs/log"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/client"
|
||||
tmserver "github.com/tendermint/tendermint/rpc/lib/server"
|
||||
cmn "github.com/tendermint/tmlibs/common"
|
||||
|
||||
client "github.com/cosmos/cosmos-sdk/client"
|
||||
keys "github.com/cosmos/cosmos-sdk/client/keys"
|
||||
rpc "github.com/cosmos/cosmos-sdk/client/rpc"
|
||||
tx "github.com/cosmos/cosmos-sdk/client/tx"
|
||||
version "github.com/cosmos/cosmos-sdk/version"
|
||||
"github.com/cosmos/cosmos-sdk/wire"
|
||||
auth "github.com/cosmos/cosmos-sdk/x/auth/rest"
|
||||
bank "github.com/cosmos/cosmos-sdk/x/bank/rest"
|
||||
)
|
||||
|
||||
const (
|
||||
flagBind = "bind"
|
||||
flagCORS = "cors"
|
||||
flagListenAddr = "laddr"
|
||||
flagCORS = "cors"
|
||||
)
|
||||
|
||||
// XXX: remove this when not needed
|
||||
func todoNotImplemented(_ *cobra.Command, _ []string) error {
|
||||
return errors.New("TODO: Command not yet implemented")
|
||||
}
|
||||
|
||||
// ServeCommand will generate a long-running rest server
|
||||
// (aka Light Client Daemon) that exposes functionality similar
|
||||
// to the cli, but over rest
|
||||
func ServeCommand() *cobra.Command {
|
||||
func ServeCommand(cdc *wire.Codec) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "serve",
|
||||
Use: "rest-server",
|
||||
Short: "Start LCD (light-client daemon), a local REST server",
|
||||
RunE: todoNotImplemented,
|
||||
RunE: startRESTServerFn(cdc),
|
||||
}
|
||||
// TODO: handle unix sockets also?
|
||||
cmd.Flags().StringP(flagBind, "b", "localhost:1317", "Interface and port that server binds to")
|
||||
cmd.Flags().StringP(flagListenAddr, "a", "tcp://localhost:1317", "Address for server to listen on")
|
||||
cmd.Flags().String(flagCORS, "", "Set to domains that can make CORS requests (* for all)")
|
||||
cmd.Flags().StringP(client.FlagChainID, "c", "", "ID of chain we connect to")
|
||||
cmd.Flags().StringP(client.FlagNode, "n", "tcp://localhost:46657", "Node to connect to")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func startRESTServerFn(cdc *wire.Codec) func(cmd *cobra.Command, args []string) error {
|
||||
return func(cmd *cobra.Command, args []string) error {
|
||||
listenAddr := viper.GetString(flagListenAddr)
|
||||
handler := createHandler(cdc)
|
||||
logger := log.NewTMLogger(log.NewSyncWriter(os.Stdout)).
|
||||
With("module", "rest-server")
|
||||
listener, err := tmserver.StartHTTPServer(listenAddr, handler, logger)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Wait forever and cleanup
|
||||
cmn.TrapSignal(func() {
|
||||
err := listener.Close()
|
||||
logger.Error("Error closing listener", "err", err)
|
||||
})
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func createHandler(cdc *wire.Codec) http.Handler {
|
||||
r := mux.NewRouter()
|
||||
r.HandleFunc("/version", version.VersionRequestHandler).Methods("GET")
|
||||
|
||||
kb, err := keys.GetKeyBase() //XXX
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// TODO make more functional? aka r = keys.RegisterRoutes(r)
|
||||
keys.RegisterRoutes(r)
|
||||
rpc.RegisterRoutes(r)
|
||||
tx.RegisterRoutes(r, cdc)
|
||||
auth.RegisterRoutes(r, cdc, "main")
|
||||
bank.RegisterRoutes(r, cdc, kb)
|
||||
return r
|
||||
}
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
package rpc
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/spf13/cobra"
|
||||
wire "github.com/tendermint/go-wire"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/client"
|
||||
)
|
||||
|
@ -18,7 +20,7 @@ func blockCommand() *cobra.Command {
|
|||
cmd := &cobra.Command{
|
||||
Use: "block [height]",
|
||||
Short: "Get verified data for a the block at given height",
|
||||
RunE: getBlock,
|
||||
RunE: printBlock,
|
||||
}
|
||||
cmd.Flags().StringP(client.FlagNode, "n", "tcp://localhost:46657", "Node to connect to")
|
||||
// TODO: change this to false when we can
|
||||
|
@ -27,7 +29,47 @@ func blockCommand() *cobra.Command {
|
|||
return cmd
|
||||
}
|
||||
|
||||
func getBlock(cmd *cobra.Command, args []string) error {
|
||||
func getBlock(height *int64) ([]byte, error) {
|
||||
// get the node
|
||||
node, err := client.GetNode()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// TODO: actually honor the --select flag!
|
||||
// header -> BlockchainInfo
|
||||
// header, tx -> Block
|
||||
// results -> BlockResults
|
||||
res, err := node.Block(height)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// TODO move maarshalling into cmd/rest functions
|
||||
// output, err := tmwire.MarshalJSON(res)
|
||||
output, err := json.MarshalIndent(res, "", " ")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return output, nil
|
||||
}
|
||||
|
||||
func GetChainHeight() (int64, error) {
|
||||
node, err := client.GetNode()
|
||||
if err != nil {
|
||||
return -1, err
|
||||
}
|
||||
status, err := node.Status()
|
||||
if err != nil {
|
||||
return -1, err
|
||||
}
|
||||
height := status.LatestBlockHeight
|
||||
return height, nil
|
||||
}
|
||||
|
||||
// CMD
|
||||
|
||||
func printBlock(cmd *cobra.Command, args []string) error {
|
||||
var height *int64
|
||||
// optional height
|
||||
if len(args) > 0 {
|
||||
|
@ -41,26 +83,51 @@ func getBlock(cmd *cobra.Command, args []string) error {
|
|||
}
|
||||
}
|
||||
|
||||
// get the node
|
||||
node, err := client.GetNode()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// TODO: actually honor the --select flag!
|
||||
// header -> BlockchainInfo
|
||||
// header, tx -> Block
|
||||
// results -> BlockResults
|
||||
res, err := node.Block(height)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
output, err := wire.MarshalJSON(res)
|
||||
// output, err := json.MarshalIndent(res, " ", "")
|
||||
output, err := getBlock(height)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println(string(output))
|
||||
return nil
|
||||
}
|
||||
|
||||
// REST
|
||||
|
||||
func BlockRequestHandler(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
height, err := strconv.ParseInt(vars["height"], 10, 64)
|
||||
if err != nil {
|
||||
w.WriteHeader(400)
|
||||
w.Write([]byte("ERROR: Couldn't parse block height. Assumed format is '/block/{height}'."))
|
||||
return
|
||||
}
|
||||
chainHeight, err := GetChainHeight()
|
||||
if height > chainHeight {
|
||||
w.WriteHeader(404)
|
||||
w.Write([]byte("ERROR: Requested block height is bigger then the chain length."))
|
||||
return
|
||||
}
|
||||
output, err := getBlock(&height)
|
||||
if err != nil {
|
||||
w.WriteHeader(500)
|
||||
w.Write([]byte(err.Error()))
|
||||
return
|
||||
}
|
||||
w.Write(output)
|
||||
}
|
||||
|
||||
func LatestBlockRequestHandler(w http.ResponseWriter, r *http.Request) {
|
||||
height, err := GetChainHeight()
|
||||
if err != nil {
|
||||
w.WriteHeader(500)
|
||||
w.Write([]byte(err.Error()))
|
||||
return
|
||||
}
|
||||
output, err := getBlock(&height)
|
||||
if err != nil {
|
||||
w.WriteHeader(500)
|
||||
w.Write([]byte(err.Error()))
|
||||
return
|
||||
}
|
||||
w.Write(output)
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package rpc
|
||||
|
||||
import (
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
|
@ -42,3 +43,12 @@ func initClientCommand() *cobra.Command {
|
|||
cmd.Flags().String(flagValHash, "", "Hash of trusted validator set (hex-encoded)")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func RegisterRoutes(r *mux.Router) {
|
||||
r.HandleFunc("/node_info", NodeInfoRequestHandler).Methods("GET")
|
||||
r.HandleFunc("/syncing", NodeSyncingRequestHandler).Methods("GET")
|
||||
r.HandleFunc("/blocks/latest", LatestBlockRequestHandler).Methods("GET")
|
||||
r.HandleFunc("/blocks/{height}", BlockRequestHandler).Methods("GET")
|
||||
r.HandleFunc("/validatorsets/latest", LatestValidatorsetRequestHandler).Methods("GET")
|
||||
r.HandleFunc("/validatorsets/{height}", ValidatorsetRequestHandler).Methods("GET")
|
||||
}
|
||||
|
|
|
@ -1,40 +1,88 @@
|
|||
package rpc
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
wire "github.com/tendermint/go-wire"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/client"
|
||||
ctypes "github.com/tendermint/tendermint/rpc/core/types"
|
||||
)
|
||||
|
||||
func statusCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "status",
|
||||
Short: "Query remote node for status",
|
||||
RunE: checkStatus,
|
||||
RunE: printNodeStatus,
|
||||
}
|
||||
cmd.Flags().StringP(client.FlagNode, "n", "tcp://localhost:46657", "Node to connect to")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func checkStatus(cmd *cobra.Command, args []string) error {
|
||||
func getNodeStatus() (*ctypes.ResultStatus, error) {
|
||||
// get the node
|
||||
node, err := client.GetNode()
|
||||
if err != nil {
|
||||
return err
|
||||
return &ctypes.ResultStatus{}, err
|
||||
}
|
||||
res, err := node.Status()
|
||||
return node.Status()
|
||||
}
|
||||
|
||||
// CMD
|
||||
|
||||
func printNodeStatus(cmd *cobra.Command, args []string) error {
|
||||
status, err := getNodeStatus()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
output, err := wire.MarshalJSON(res)
|
||||
output, err := wire.MarshalJSON(status)
|
||||
// output, err := json.MarshalIndent(res, " ", "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println(string(output))
|
||||
return nil
|
||||
}
|
||||
|
||||
// REST
|
||||
|
||||
func NodeInfoRequestHandler(w http.ResponseWriter, r *http.Request) {
|
||||
status, err := getNodeStatus()
|
||||
if err != nil {
|
||||
w.WriteHeader(500)
|
||||
w.Write([]byte(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
nodeInfo := status.NodeInfo
|
||||
output, err := json.MarshalIndent(nodeInfo, "", " ")
|
||||
if err != nil {
|
||||
w.WriteHeader(500)
|
||||
w.Write([]byte(err.Error()))
|
||||
return
|
||||
}
|
||||
w.Write(output)
|
||||
}
|
||||
|
||||
func NodeSyncingRequestHandler(w http.ResponseWriter, r *http.Request) {
|
||||
status, err := getNodeStatus()
|
||||
if err != nil {
|
||||
w.WriteHeader(500)
|
||||
w.Write([]byte(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
syncing := status.Syncing
|
||||
if err != nil {
|
||||
w.WriteHeader(500)
|
||||
w.Write([]byte(err.Error()))
|
||||
return
|
||||
}
|
||||
w.Write([]byte(strconv.FormatBool(syncing)))
|
||||
}
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
package rpc
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/spf13/cobra"
|
||||
wire "github.com/tendermint/go-wire"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/client"
|
||||
)
|
||||
|
@ -14,7 +16,7 @@ func validatorCommand() *cobra.Command {
|
|||
cmd := &cobra.Command{
|
||||
Use: "validatorset <height>",
|
||||
Short: "Get the full validator set at given height",
|
||||
RunE: getValidators,
|
||||
RunE: printValidators,
|
||||
}
|
||||
cmd.Flags().StringP(client.FlagNode, "n", "tcp://localhost:46657", "Node to connect to")
|
||||
// TODO: change this to false when we can
|
||||
|
@ -22,7 +24,28 @@ func validatorCommand() *cobra.Command {
|
|||
return cmd
|
||||
}
|
||||
|
||||
func getValidators(cmd *cobra.Command, args []string) error {
|
||||
func getValidators(height *int64) ([]byte, error) {
|
||||
// get the node
|
||||
node, err := client.GetNode()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res, err := node.Validators(height)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
output, err := json.MarshalIndent(res, "", " ")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return output, nil
|
||||
}
|
||||
|
||||
// CMD
|
||||
|
||||
func printValidators(cmd *cobra.Command, args []string) error {
|
||||
var height *int64
|
||||
// optional height
|
||||
if len(args) > 0 {
|
||||
|
@ -36,22 +59,52 @@ func getValidators(cmd *cobra.Command, args []string) error {
|
|||
}
|
||||
}
|
||||
|
||||
// get the node
|
||||
node, err := client.GetNode()
|
||||
output, err := getValidators(height)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
res, err := node.Validators(height)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
output, err := wire.MarshalJSON(res)
|
||||
// output, err := json.MarshalIndent(res, " ", "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println(string(output))
|
||||
return nil
|
||||
}
|
||||
|
||||
// REST
|
||||
|
||||
func ValidatorsetRequestHandler(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
height, err := strconv.ParseInt(vars["height"], 10, 64)
|
||||
if err != nil {
|
||||
w.WriteHeader(400)
|
||||
w.Write([]byte("ERROR: Couldn't parse block height. Assumed format is '/validatorsets/{height}'."))
|
||||
return
|
||||
}
|
||||
chainHeight, err := GetChainHeight()
|
||||
if height > chainHeight {
|
||||
w.WriteHeader(404)
|
||||
w.Write([]byte("ERROR: Requested block height is bigger then the chain length."))
|
||||
return
|
||||
}
|
||||
output, err := getValidators(&height)
|
||||
if err != nil {
|
||||
w.WriteHeader(500)
|
||||
w.Write([]byte(err.Error()))
|
||||
return
|
||||
}
|
||||
w.Write(output)
|
||||
}
|
||||
|
||||
func LatestValidatorsetRequestHandler(w http.ResponseWriter, r *http.Request) {
|
||||
height, err := GetChainHeight()
|
||||
if err != nil {
|
||||
w.WriteHeader(500)
|
||||
w.Write([]byte(err.Error()))
|
||||
return
|
||||
}
|
||||
output, err := getValidators(&height)
|
||||
if err != nil {
|
||||
w.WriteHeader(500)
|
||||
w.Write([]byte(err.Error()))
|
||||
return
|
||||
}
|
||||
w.Write(output)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
package tx
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/client/builder"
|
||||
)
|
||||
|
||||
type BroadcastTxBody struct {
|
||||
TxBytes string `json="tx"`
|
||||
}
|
||||
|
||||
func BroadcastTxRequestHandler(w http.ResponseWriter, r *http.Request) {
|
||||
var m BroadcastTxBody
|
||||
|
||||
decoder := json.NewDecoder(r.Body)
|
||||
err := decoder.Decode(&m)
|
||||
if err != nil {
|
||||
w.WriteHeader(400)
|
||||
w.Write([]byte(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
res, err := builder.BroadcastTx([]byte(m.TxBytes))
|
||||
if err != nil {
|
||||
w.WriteHeader(500)
|
||||
w.Write([]byte(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
w.Write([]byte(string(res.Height)))
|
||||
}
|
|
@ -4,11 +4,13 @@ import (
|
|||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
|
||||
abci "github.com/tendermint/abci/types"
|
||||
ctypes "github.com/tendermint/tendermint/rpc/core/types"
|
||||
|
||||
|
@ -22,7 +24,7 @@ func QueryTxCmd(cmdr commander) *cobra.Command {
|
|||
cmd := &cobra.Command{
|
||||
Use: "tx [hash]",
|
||||
Short: "Matches this txhash over all committed blocks",
|
||||
RunE: cmdr.queryTxCmd,
|
||||
RunE: cmdr.queryAndPrintTx,
|
||||
}
|
||||
cmd.Flags().StringP(client.FlagNode, "n", "tcp://localhost:46657", "Node to connect to")
|
||||
// TODO: change this to false when we can
|
||||
|
@ -30,42 +32,28 @@ func QueryTxCmd(cmdr commander) *cobra.Command {
|
|||
return cmd
|
||||
}
|
||||
|
||||
// command to query for a transaction
|
||||
func (c commander) queryTxCmd(cmd *cobra.Command, args []string) error {
|
||||
if len(args) != 1 || len(args[0]) == 0 {
|
||||
return errors.New("You must provide a tx hash")
|
||||
}
|
||||
|
||||
// find the key to look up the account
|
||||
hexStr := args[0]
|
||||
hash, err := hex.DecodeString(hexStr)
|
||||
func (c commander) queryTx(hashHexStr string, trustNode bool) ([]byte, error) {
|
||||
hash, err := hex.DecodeString(hashHexStr)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// get the node
|
||||
node, err := client.GetNode()
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
prove := !viper.GetBool(client.FlagTrustNode)
|
||||
|
||||
res, err := node.Tx(hash, prove)
|
||||
res, err := node.Tx(hash, !trustNode)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
info, err := formatTxResult(c.cdc, res)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
output, err := json.MarshalIndent(info, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println(string(output))
|
||||
|
||||
return nil
|
||||
return json.MarshalIndent(info, "", " ")
|
||||
}
|
||||
|
||||
func formatTxResult(cdc *wire.Codec, res *ctypes.ResultTx) (txInfo, error) {
|
||||
|
@ -98,3 +86,47 @@ func parseTx(cdc *wire.Codec, txBytes []byte) (sdk.Tx, error) {
|
|||
}
|
||||
return tx, nil
|
||||
}
|
||||
|
||||
// CMD
|
||||
|
||||
// command to query for a transaction
|
||||
func (c commander) queryAndPrintTx(cmd *cobra.Command, args []string) error {
|
||||
if len(args) != 1 || len(args[0]) == 0 {
|
||||
return errors.New("You must provide a tx hash")
|
||||
}
|
||||
|
||||
// find the key to look up the account
|
||||
hashHexStr := args[0]
|
||||
trustNode := viper.GetBool(client.FlagTrustNode)
|
||||
|
||||
output, err := c.queryTx(hashHexStr, trustNode)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println(string(output))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// REST
|
||||
|
||||
func QueryTxRequestHandler(cdc *wire.Codec) func(http.ResponseWriter, *http.Request) {
|
||||
c := commander{cdc}
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
hashHexStr := vars["hash"]
|
||||
trustNode, err := strconv.ParseBool(r.FormValue("trust_node"))
|
||||
// trustNode defaults to true
|
||||
if err != nil {
|
||||
trustNode = true
|
||||
}
|
||||
|
||||
output, err := c.queryTx(hashHexStr, trustNode)
|
||||
if err != nil {
|
||||
w.WriteHeader(500)
|
||||
w.Write([]byte(err.Error()))
|
||||
return
|
||||
}
|
||||
w.Write(output)
|
||||
}
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
package tx
|
||||
|
||||
import (
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/wire"
|
||||
|
@ -19,3 +20,10 @@ func AddCommands(cmd *cobra.Command, cdc *wire.Codec) {
|
|||
QueryTxCmd(cmdr),
|
||||
)
|
||||
}
|
||||
|
||||
func RegisterRoutes(r *mux.Router, cdc *wire.Codec) {
|
||||
// r.HandleFunc("/txs", SearchTxRequestHandler(cdc)).Methods("GET")
|
||||
r.HandleFunc("/txs/{hash}", QueryTxRequestHandler(cdc)).Methods("GET")
|
||||
// r.HandleFunc("/txs/sign", SignTxRequstHandler).Methods("POST")
|
||||
// r.HandleFunc("/txs/broadcast", BroadcastTxRequestHandler).Methods("POST")
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ package tx
|
|||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
@ -24,7 +25,7 @@ func SearchTxCmd(cmdr commander) *cobra.Command {
|
|||
cmd := &cobra.Command{
|
||||
Use: "txs",
|
||||
Short: "Search for all transactions that match the given tags",
|
||||
RunE: cmdr.searchTxCmd,
|
||||
RunE: cmdr.searchAndPrintTx,
|
||||
}
|
||||
cmd.Flags().StringP(client.FlagNode, "n", "tcp://localhost:46657", "Node to connect to")
|
||||
// TODO: change this to false once proofs built in
|
||||
|
@ -34,10 +35,9 @@ func SearchTxCmd(cmdr commander) *cobra.Command {
|
|||
return cmd
|
||||
}
|
||||
|
||||
func (c commander) searchTxCmd(cmd *cobra.Command, args []string) error {
|
||||
tags := viper.GetStringSlice(flagTags)
|
||||
func (c commander) searchTx(tags []string) ([]byte, error) {
|
||||
if len(tags) == 0 {
|
||||
return errors.New("Must declare at least one tag to search")
|
||||
return nil, errors.New("Must declare at least one tag to search")
|
||||
}
|
||||
// XXX: implement ANY
|
||||
query := strings.Join(tags, " AND ")
|
||||
|
@ -45,27 +45,25 @@ func (c commander) searchTxCmd(cmd *cobra.Command, args []string) error {
|
|||
// get the node
|
||||
node, err := client.GetNode()
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
prove := !viper.GetBool(client.FlagTrustNode)
|
||||
res, err := node.TxSearch(query, prove)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
info, err := formatTxResults(c.cdc, res)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
output, err := c.cdc.MarshalJSON(info)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
fmt.Println(string(output))
|
||||
|
||||
return nil
|
||||
return output, nil
|
||||
}
|
||||
|
||||
func formatTxResults(cdc *wire.Codec, res []*ctypes.ResultTx) ([]txInfo, error) {
|
||||
|
@ -79,3 +77,40 @@ func formatTxResults(cdc *wire.Codec, res []*ctypes.ResultTx) ([]txInfo, error)
|
|||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// CMD
|
||||
|
||||
func (c commander) searchAndPrintTx(cmd *cobra.Command, args []string) error {
|
||||
tags := viper.GetStringSlice(flagTags)
|
||||
|
||||
output, err := c.searchTx(tags)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println(string(output))
|
||||
return nil
|
||||
}
|
||||
|
||||
// REST
|
||||
|
||||
func SearchTxRequestHandler(cdc *wire.Codec) func(http.ResponseWriter, *http.Request) {
|
||||
c := commander{cdc}
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
tag := r.FormValue("tag")
|
||||
if tag == "" {
|
||||
w.WriteHeader(400)
|
||||
w.Write([]byte("You need to provide a tag to search for."))
|
||||
return
|
||||
}
|
||||
|
||||
tags := []string{tag}
|
||||
output, err := c.searchTx(tags)
|
||||
if err != nil {
|
||||
w.WriteHeader(500)
|
||||
w.Write([]byte(err.Error()))
|
||||
return
|
||||
}
|
||||
w.Write(output)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
package tx
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
keybase "github.com/cosmos/cosmos-sdk/client/keys"
|
||||
keys "github.com/tendermint/go-crypto/keys"
|
||||
)
|
||||
|
||||
type SignTxBody struct {
|
||||
Name string `json="name"`
|
||||
Password string `json="password"`
|
||||
TxBytes string `json="tx"`
|
||||
}
|
||||
|
||||
func SignTxRequstHandler(w http.ResponseWriter, r *http.Request) {
|
||||
var kb keys.Keybase
|
||||
var m SignTxBody
|
||||
|
||||
decoder := json.NewDecoder(r.Body)
|
||||
err := decoder.Decode(&m)
|
||||
if err != nil {
|
||||
w.WriteHeader(400)
|
||||
w.Write([]byte(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
kb, err = keybase.GetKeyBase()
|
||||
if err != nil {
|
||||
w.WriteHeader(500)
|
||||
w.Write([]byte(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
//TODO check if account exists
|
||||
sig, _, err := kb.Sign(m.Name, m.Password, []byte(m.TxBytes))
|
||||
if err != nil {
|
||||
w.WriteHeader(403)
|
||||
w.Write([]byte(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
w.Write(sig.Bytes())
|
||||
}
|
|
@ -105,14 +105,6 @@ type Tx interface {
|
|||
|
||||
GetMsg() Msg
|
||||
|
||||
// The address that pays the base fee for this message. The fee is
|
||||
// deducted before the Msg is processed.
|
||||
GetFeePayer() Address
|
||||
|
||||
// Get the canonical byte representation of the Tx.
|
||||
// Includes any signatures (or empty slots).
|
||||
GetTxBytes() []byte
|
||||
|
||||
// Signatures returns the signature of signers who signed the Msg.
|
||||
// CONTRACT: Length returned is same as length of
|
||||
// pubkeys returned from MsgKeySigners, and the order
|
||||
|
@ -148,8 +140,9 @@ case of Basecoin, the public key only needs to be included in the first
|
|||
transaction send by a given account - after that, the public key is forever
|
||||
stored by the application and can be left out of transactions.
|
||||
|
||||
Transactions can also specify the address responsible for paying the
|
||||
transaction's fees using the `tx.GetFeePayer()` method.
|
||||
The address responsible for paying the transactions fee is the first address
|
||||
returned by msg.GetSigners(). The convenience function `FeePayer(tx Tx)` is provided
|
||||
to return this.
|
||||
|
||||
The standard way to create a transaction from a message is to use the `StdTx`:
|
||||
|
||||
|
|
|
@ -46,6 +46,14 @@ paths:
|
|||
other:
|
||||
description: more information on versions
|
||||
type: array
|
||||
/syncing:
|
||||
get:
|
||||
summary: Syncing state of node
|
||||
description: Get if the node is currently syning with other nodes
|
||||
responses:
|
||||
200:
|
||||
description: "true" or "false"
|
||||
|
||||
/keys:
|
||||
get:
|
||||
summary: List of accounts stored locally
|
||||
|
@ -62,7 +70,7 @@ paths:
|
|||
summary: Create a new account locally
|
||||
responses:
|
||||
200:
|
||||
description: OK
|
||||
description: Returns address of the account created
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
|
@ -117,9 +125,12 @@ paths:
|
|||
schema:
|
||||
type: object
|
||||
required:
|
||||
- password
|
||||
- new_password
|
||||
- old_password
|
||||
properties:
|
||||
password:
|
||||
new_password:
|
||||
type: string
|
||||
old_password:
|
||||
type: string
|
||||
responses:
|
||||
200:
|
||||
|
@ -147,35 +158,35 @@ paths:
|
|||
description: Password is wrong
|
||||
404:
|
||||
description: Account is not available
|
||||
/accounts/send:
|
||||
post:
|
||||
summary: Send coins (build -> sign -> send)
|
||||
security:
|
||||
- sign: []
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
fees:
|
||||
$ref: "#/components/schemas/Coins"
|
||||
outputs:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
pub_key:
|
||||
$ref: "#/components/schemas/PubKey"
|
||||
amount:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/Coins"
|
||||
responses:
|
||||
202:
|
||||
description: Tx was send and will probably be added to the next block
|
||||
400:
|
||||
description: The Tx was malformated
|
||||
# /accounts/send:
|
||||
# post:
|
||||
# summary: Send coins (build -> sign -> send)
|
||||
# security:
|
||||
# - sign: []
|
||||
# requestBody:
|
||||
# content:
|
||||
# application/json:
|
||||
# schema:
|
||||
# type: object
|
||||
# properties:
|
||||
# fees:
|
||||
# $ref: "#/components/schemas/Coins"
|
||||
# outputs:
|
||||
# type: array
|
||||
# items:
|
||||
# type: object
|
||||
# properties:
|
||||
# pub_key:
|
||||
# $ref: "#/components/schemas/PubKey"
|
||||
# amount:
|
||||
# type: array
|
||||
# items:
|
||||
# $ref: "#/components/schemas/Coins"
|
||||
# responses:
|
||||
# 202:
|
||||
# description: Tx was send and will probably be added to the next block
|
||||
# 400:
|
||||
# description: The Tx was malformated
|
||||
|
||||
/accounts/{address}:
|
||||
parameters:
|
||||
|
@ -214,12 +225,18 @@ paths:
|
|||
schema:
|
||||
type: object
|
||||
properties:
|
||||
fees:
|
||||
$ref: "#/components/schemas/Coins"
|
||||
name:
|
||||
type: string
|
||||
password:
|
||||
type: string
|
||||
amount:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/Coins"
|
||||
chain_id:
|
||||
type: string
|
||||
squence:
|
||||
type: number
|
||||
responses:
|
||||
202:
|
||||
description: Tx was send and will probably be added to the next block
|
||||
|
@ -300,72 +317,72 @@ paths:
|
|||
$ref: "#/components/schemas/Delegate"
|
||||
404:
|
||||
description: Block at height not available
|
||||
/txs:
|
||||
parameters:
|
||||
- in: query
|
||||
name: tag
|
||||
schema:
|
||||
type: string
|
||||
example: "coin.sender=EE5F3404034C524501629B56E0DDC38FAD651F04"
|
||||
required: true
|
||||
- in: query
|
||||
name: page
|
||||
description: Pagination page
|
||||
schema:
|
||||
type: number
|
||||
default: 0
|
||||
- in: query
|
||||
name: size
|
||||
description: Pagination size
|
||||
schema:
|
||||
type: number
|
||||
default: 50
|
||||
get:
|
||||
summary: Query Tx
|
||||
responses:
|
||||
200:
|
||||
description: All Tx matching the provided tags
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/Tx"
|
||||
404:
|
||||
description: Pagination is out of bounds
|
||||
/txs/sign:
|
||||
post:
|
||||
summary: Sign a Tx
|
||||
description: Sign a Tx providing locally stored account and according password
|
||||
security:
|
||||
- sign: []
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/TxBuild"
|
||||
responses:
|
||||
200:
|
||||
description: The signed Tx
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/TxSigned"
|
||||
401:
|
||||
description: Account name and/or password where wrong
|
||||
/txs/broadcast:
|
||||
post:
|
||||
summary: Send signed Tx
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/TxSigned"
|
||||
responses:
|
||||
202:
|
||||
description: Tx was send and will probably be added to the next block
|
||||
400:
|
||||
description: The Tx was malformated
|
||||
# /txs:
|
||||
# parameters:
|
||||
# - in: query
|
||||
# name: tag
|
||||
# schema:
|
||||
# type: string
|
||||
# example: "coin.sender=EE5F3404034C524501629B56E0DDC38FAD651F04"
|
||||
# required: true
|
||||
# - in: query
|
||||
# name: page
|
||||
# description: Pagination page
|
||||
# schema:
|
||||
# type: number
|
||||
# default: 0
|
||||
# - in: query
|
||||
# name: size
|
||||
# description: Pagination size
|
||||
# schema:
|
||||
# type: number
|
||||
# default: 50
|
||||
# get:
|
||||
# summary: Query Tx
|
||||
# responses:
|
||||
# 200:
|
||||
# description: All Tx matching the provided tags
|
||||
# content:
|
||||
# application/json:
|
||||
# schema:
|
||||
# type: array
|
||||
# items:
|
||||
# $ref: "#/components/schemas/Tx"
|
||||
# 404:
|
||||
# description: Pagination is out of bounds
|
||||
# /txs/sign:
|
||||
# post:
|
||||
# summary: Sign a Tx
|
||||
# description: Sign a Tx providing locally stored account and according password
|
||||
# security:
|
||||
# - sign: []
|
||||
# requestBody:
|
||||
# content:
|
||||
# application/json:
|
||||
# schema:
|
||||
# $ref: "#/components/schemas/TxBuild"
|
||||
# responses:
|
||||
# 200:
|
||||
# description: The signed Tx
|
||||
# content:
|
||||
# application/json:
|
||||
# schema:
|
||||
# $ref: "#/components/schemas/TxSigned"
|
||||
# 401:
|
||||
# description: Account name and/or password where wrong
|
||||
# /txs/broadcast:
|
||||
# post:
|
||||
# summary: Send signed Tx
|
||||
# requestBody:
|
||||
# content:
|
||||
# application/json:
|
||||
# schema:
|
||||
# $ref: "#/components/schemas/TxSigned"
|
||||
# responses:
|
||||
# 202:
|
||||
# description: Tx was send and will probably be added to the next block
|
||||
# 400:
|
||||
# description: The Tx was malformated
|
||||
/txs/{hash}:
|
||||
parameters:
|
||||
- in: path
|
||||
|
@ -385,140 +402,140 @@ paths:
|
|||
$ref: "#/components/schemas/Tx"
|
||||
404:
|
||||
description: Tx not available for provided hash
|
||||
/delegates:
|
||||
parameters:
|
||||
- in: query
|
||||
name: delegator
|
||||
description: Query for all delegates a delegator has stake with
|
||||
schema:
|
||||
$ref: "#/components/schemas/Address"
|
||||
get:
|
||||
summary: Get a list of canidates/delegates/validators (optionally filtered by delegator)
|
||||
responses:
|
||||
200:
|
||||
description: List of delegates, filtered by provided delegator address
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/Delegate"
|
||||
/delegates/bond:
|
||||
post:
|
||||
summary: Bond atoms (build -> sign -> send)
|
||||
security:
|
||||
- sign: []
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
amount:
|
||||
$ref: "#/components/schemas/Coins"
|
||||
pub_key:
|
||||
$ref: "#/components/schemas/PubKey"
|
||||
responses:
|
||||
202:
|
||||
description: Tx was send and will probably be added to the next block
|
||||
400:
|
||||
description: The Tx was malformated
|
||||
/delegates/unbond:
|
||||
post:
|
||||
summary: Unbond atoms (build -> sign -> send)
|
||||
security:
|
||||
- sign: []
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
amount:
|
||||
$ref: "#/components/schemas/Coins"
|
||||
pub_key:
|
||||
$ref: "#/components/schemas/PubKey"
|
||||
responses:
|
||||
202:
|
||||
description: Tx was send and will probably be added to the next block
|
||||
400:
|
||||
description: The Tx was malformated
|
||||
/delegates/{pubkey}:
|
||||
parameters:
|
||||
- in: path
|
||||
name: pubkey
|
||||
description: Pubkey of a delegate
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
example: 81B11E717789600CC192B26F452A983DF13B985EE75ABD9DD9E68D7BA007A958
|
||||
get:
|
||||
summary: Get a certain canidate/delegate/validator
|
||||
responses:
|
||||
200:
|
||||
description: Delegate for specified pub_key
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Delegate"
|
||||
404:
|
||||
description: No delegate found for provided pub_key
|
||||
/delegates/{pubkey}/bond:
|
||||
parameters:
|
||||
- in: path
|
||||
name: pubkey
|
||||
description: Pubkey of a delegate
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
example: 81B11E717789600CC192B26F452A983DF13B985EE75ABD9DD9E68D7BA007A958
|
||||
post:
|
||||
summary: Bond atoms (build -> sign -> send)
|
||||
security:
|
||||
- sign: []
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
amount:
|
||||
$ref: "#/components/schemas/Coins"
|
||||
responses:
|
||||
202:
|
||||
description: Tx was send and will probably be added to the next block
|
||||
400:
|
||||
description: The Tx was malformated
|
||||
/delegates/{pubkey}/unbond:
|
||||
parameters:
|
||||
- in: path
|
||||
name: pubkey
|
||||
description: Pubkey of a delegate
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
example: 81B11E717789600CC192B26F452A983DF13B985EE75ABD9DD9E68D7BA007A958
|
||||
post:
|
||||
summary: Unbond atoms (build -> sign -> send)
|
||||
security:
|
||||
- sign: []
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
amount:
|
||||
$ref: "#/components/schemas/Coins"
|
||||
responses:
|
||||
202:
|
||||
description: Tx was send and will probably be added to the next block
|
||||
400:
|
||||
description: The Tx was malformated
|
||||
# /delegates:
|
||||
# parameters:
|
||||
# - in: query
|
||||
# name: delegator
|
||||
# description: Query for all delegates a delegator has stake with
|
||||
# schema:
|
||||
# $ref: "#/components/schemas/Address"
|
||||
# get:
|
||||
# summary: Get a list of canidates/delegates/validators (optionally filtered by delegator)
|
||||
# responses:
|
||||
# 200:
|
||||
# description: List of delegates, filtered by provided delegator address
|
||||
# content:
|
||||
# application/json:
|
||||
# schema:
|
||||
# type: array
|
||||
# items:
|
||||
# $ref: "#/components/schemas/Delegate"
|
||||
# /delegates/bond:
|
||||
# post:
|
||||
# summary: Bond atoms (build -> sign -> send)
|
||||
# security:
|
||||
# - sign: []
|
||||
# requestBody:
|
||||
# content:
|
||||
# application/json:
|
||||
# schema:
|
||||
# type: array
|
||||
# items:
|
||||
# type: object
|
||||
# properties:
|
||||
# amount:
|
||||
# $ref: "#/components/schemas/Coins"
|
||||
# pub_key:
|
||||
# $ref: "#/components/schemas/PubKey"
|
||||
# responses:
|
||||
# 202:
|
||||
# description: Tx was send and will probably be added to the next block
|
||||
# 400:
|
||||
# description: The Tx was malformated
|
||||
# /delegates/unbond:
|
||||
# post:
|
||||
# summary: Unbond atoms (build -> sign -> send)
|
||||
# security:
|
||||
# - sign: []
|
||||
# requestBody:
|
||||
# content:
|
||||
# application/json:
|
||||
# schema:
|
||||
# type: array
|
||||
# items:
|
||||
# type: object
|
||||
# properties:
|
||||
# amount:
|
||||
# $ref: "#/components/schemas/Coins"
|
||||
# pub_key:
|
||||
# $ref: "#/components/schemas/PubKey"
|
||||
# responses:
|
||||
# 202:
|
||||
# description: Tx was send and will probably be added to the next block
|
||||
# 400:
|
||||
# description: The Tx was malformated
|
||||
# /delegates/{pubkey}:
|
||||
# parameters:
|
||||
# - in: path
|
||||
# name: pubkey
|
||||
# description: Pubkey of a delegate
|
||||
# required: true
|
||||
# schema:
|
||||
# type: string
|
||||
# example: 81B11E717789600CC192B26F452A983DF13B985EE75ABD9DD9E68D7BA007A958
|
||||
# get:
|
||||
# summary: Get a certain canidate/delegate/validator
|
||||
# responses:
|
||||
# 200:
|
||||
# description: Delegate for specified pub_key
|
||||
# content:
|
||||
# application/json:
|
||||
# schema:
|
||||
# $ref: "#/components/schemas/Delegate"
|
||||
# 404:
|
||||
# description: No delegate found for provided pub_key
|
||||
# /delegates/{pubkey}/bond:
|
||||
# parameters:
|
||||
# - in: path
|
||||
# name: pubkey
|
||||
# description: Pubkey of a delegate
|
||||
# required: true
|
||||
# schema:
|
||||
# type: string
|
||||
# example: 81B11E717789600CC192B26F452A983DF13B985EE75ABD9DD9E68D7BA007A958
|
||||
# post:
|
||||
# summary: Bond atoms (build -> sign -> send)
|
||||
# security:
|
||||
# - sign: []
|
||||
# requestBody:
|
||||
# content:
|
||||
# application/json:
|
||||
# schema:
|
||||
# type: object
|
||||
# properties:
|
||||
# amount:
|
||||
# $ref: "#/components/schemas/Coins"
|
||||
# responses:
|
||||
# 202:
|
||||
# description: Tx was send and will probably be added to the next block
|
||||
# 400:
|
||||
# description: The Tx was malformated
|
||||
# /delegates/{pubkey}/unbond:
|
||||
# parameters:
|
||||
# - in: path
|
||||
# name: pubkey
|
||||
# description: Pubkey of a delegate
|
||||
# required: true
|
||||
# schema:
|
||||
# type: string
|
||||
# example: 81B11E717789600CC192B26F452A983DF13B985EE75ABD9DD9E68D7BA007A958
|
||||
# post:
|
||||
# summary: Unbond atoms (build -> sign -> send)
|
||||
# security:
|
||||
# - sign: []
|
||||
# requestBody:
|
||||
# content:
|
||||
# application/json:
|
||||
# schema:
|
||||
# type: object
|
||||
# properties:
|
||||
# amount:
|
||||
# $ref: "#/components/schemas/Coins"
|
||||
# responses:
|
||||
# 202:
|
||||
# description: Tx was send and will probably be added to the next block
|
||||
# 400:
|
||||
# description: The Tx was malformated
|
||||
|
||||
components:
|
||||
schemas:
|
||||
|
|
|
@ -219,14 +219,6 @@ A transaction is a message with additional information for authentication:
|
|||
|
||||
GetMsg() Msg
|
||||
|
||||
// The address that pays the base fee for this message. The fee is
|
||||
// deducted before the Msg is processed.
|
||||
GetFeePayer() Address
|
||||
|
||||
// Get the canonical byte representation of the Tx.
|
||||
// Includes any signatures (or empty slots).
|
||||
GetTxBytes() []byte
|
||||
|
||||
// Signatures returns the signature of signers who signed the Msg.
|
||||
// CONTRACT: Length returned is same as length of
|
||||
// pubkeys returned from MsgKeySigners, and the order
|
||||
|
@ -261,9 +253,6 @@ case of Basecoin, the public key only needs to be included in the first
|
|||
transaction send by a given account - after that, the public key is forever
|
||||
stored by the application and can be left out of transactions.
|
||||
|
||||
Transactions can also specify the address responsible for paying the
|
||||
transaction's fees using the ``tx.GetFeePayer()`` method.
|
||||
|
||||
The standard way to create a transaction from a message is to use the ``StdTx``:
|
||||
|
||||
::
|
||||
|
|
|
@ -0,0 +1,659 @@
|
|||
# Governance documentation
|
||||
|
||||
*Disclaimer: This is work in progress. Mechanisms are susceptible to change.*
|
||||
|
||||
This document describes the high-level architecture of the governance module. The governance module allows bonded Atom holders to vote on proposals on a 1 bonded Atom 1 vote basis.
|
||||
|
||||
## Design overview
|
||||
|
||||
The governance process is divided in a few steps that are outlined below:
|
||||
|
||||
- **Proposal submission:** Proposal is submitted to the blockchain with a deposit
|
||||
- **Vote:** Once deposit reaches a certain value (`MinDeposit`), proposal is confirmed and vote opens. Bonded Atom holders can then send `TxGovVote` transactions to vote on the proposal
|
||||
- If the proposal involves a software upgrade:
|
||||
- **Signal:** Validators start signaling that they are ready to switch to the new version
|
||||
- **Switch:** Once more than 75% of validators have signaled that they are ready to switch, their software automatically flips to the new version
|
||||
|
||||
## Proposal submission
|
||||
|
||||
### Right to submit a proposal
|
||||
|
||||
Any Atom holder, whether bonded or unbonded, can submit proposals by sending a `TxGovProposal` transaction. Once a proposal is submitted, it is identified by its unique `proposalID`.
|
||||
|
||||
### Proposal filter (minimum deposit)
|
||||
|
||||
To prevent spam, proposals must be submitted with a deposit in Atoms. Voting period will not start as long as the proposal's deposit is smaller than the minimum deposit `MinDeposit`.
|
||||
|
||||
When a proposal is submitted, it has to be accompagnied by a deposit that must be strictly positive but can be inferior to `MinDeposit`. Indeed, the submitter need not pay for the entire deposit on its own. If a proposal's deposit is strictly inferior to `MinDeposit`, other Atom holders can increase the proposal's deposit by sending a `TxGovDeposit` transaction. Once the proposals's deposit reaches `MinDeposit`, it enters voting period.
|
||||
|
||||
### Deposit refund
|
||||
|
||||
There are two instances where Atom holders that deposited can claim back their deposit:
|
||||
- If the proposal is accepted
|
||||
- If the proposal's deposit does not reach `MinDeposit` for a period longer than `MaxDepositPeriod` (initial value: 2 months). Then the proposal is considered closed and nobody can deposit on it anymore.
|
||||
|
||||
In such instances, Atom holders that deposited can send a `TxGovClaimDeposit` transaction to retrieve their share of the deposit.
|
||||
|
||||
### Proposal types
|
||||
|
||||
In the initial version of the governance module, there are two types of proposal:
|
||||
- `PlainTextProposal`. All the proposals that do not involve a modification of the source code go under this type. For example, an opinion poll would use a proposal of type `PlainTextProposal`
|
||||
- `SoftwareUpgradeProposal`. If accepted, validators are expected to update their software in accordance with the proposal. They must do so by following a 2-steps process described in the [Software Upgrade](#software-upgrade) section below. Software upgrade roadmap may be discussed and agreed on via `PlainTextProposals`, but actual software upgrades must be performed via `SoftwareUpgradeProposals`.
|
||||
|
||||
### Proposal categories
|
||||
|
||||
There are two categories of proposal:
|
||||
- `Regular`
|
||||
- `Urgent`
|
||||
|
||||
These two categories are strictly identical except that `Urgent` proposals can be accepted faster if a certain condition is met. For more information, see [Threshold](#threshold) section.
|
||||
|
||||
## Vote
|
||||
|
||||
### Participants
|
||||
|
||||
*Participants* are users that have the right to vote on proposals. On the Cosmos Hub, participants are bonded Atom holders. Unbonded Atom holders and other users do not get the right to participate in governance. However, they can submit and deposit on proposals.
|
||||
|
||||
Note that some *participants* can be forbidden to vote on a proposal under a certain validator if:
|
||||
- *participant* bonded or unbonded Atoms to said validator after proposal entered voting period
|
||||
- *participant* became validator after proposal entered voting period
|
||||
|
||||
This does not prevent *participant* to vote with Atoms bonded to other validators. For example, if a *participant* bonded some Atoms to validator A before a proposal entered voting period and other Atoms to validator B after proposal entered voting period, only the vote under validator B will be forbidden.
|
||||
|
||||
### Voting period
|
||||
|
||||
Once a proposal reaches `MinDeposit`, it immediately enters `Voting period`. We define `Voting period` as the interval between the moment the vote opens and the moment the vote closes. `Voting period` should always be shorter than `Unbonding period` to prevent double voting. The initial value of `Voting period` is 2 weeks.
|
||||
|
||||
### Option set
|
||||
|
||||
The option set of a proposal refers to the set of choices a participant can choose from when casting its vote.
|
||||
|
||||
The initial option set includes the following options:
|
||||
- `Yes`
|
||||
- `No`
|
||||
- `NoWithVeto`
|
||||
- `Abstain`
|
||||
|
||||
`NoWithVeto` counts as `No` but also adds a `Veto` vote. `Abstain` option allows voters to signal that they do not intend to vote in favor or against the proposal but accept the result of the vote.
|
||||
|
||||
*Note: from the UI, for urgent proposals we should maybe add a ‘Not Urgent’ option that casts a `NoWithVeto` vote.*
|
||||
|
||||
### Quorum
|
||||
|
||||
Quorum is defined as the minimum percentage of voting power that needs to be casted on a proposal for the result to be valid.
|
||||
|
||||
In the initial version of the governance module, there will be no quorum enforced by the protocol. Participation is ensured via the combination of inheritance and validator's punishment for non-voting.
|
||||
|
||||
### Threshold
|
||||
|
||||
Threshold is defined as the minimum proportion of `Yes` votes (excluding `Abstain` votes) for the proposal to be accepted.
|
||||
|
||||
Initially, the threshold is set at 50% with a possibility to veto if more than 1/3rd of votes (excluding `Abstain` votes) are `NoWithVeto` votes. This means that proposals are accepted if the proportion of `Yes` votes (excluding `Abstain` votes) at the end of the voting period is superior to 50% and if the proportion of `NoWithVeto` votes is inferior to 1/3 (excluding `Abstain` votes).
|
||||
|
||||
`Urgent` proposals also work with the aforementioned threshold, except there is another condition that can accelerate the acceptance of the proposal. Namely, if the ratio of `Yes` votes to `InitTotalVotingPower` exceeds 2:3, `UrgentProposal` will be immediately accepted, even if the `Voting period` is not finished. `InitTotalVotingPower` is the total voting power of all bonded Atom holders at the moment when the vote opens.
|
||||
|
||||
### Inheritance
|
||||
|
||||
If a delegator does not vote, it will inherit its validator vote.
|
||||
|
||||
- If the delegator votes before its validator, it will not inherit from the validator's vote.
|
||||
- If the delegator votes after its validator, it will override its validator vote with its own. If the proposal is a `Urgent` proposal, it is possible that the vote will close before delegators have a chance to react and override their validator's vote. This is not a problem, as `Urgent` proposals require more than 2/3rd of the total voting power to pass before the end of the voting period. If more than 2/3rd of validators collude, they can censor the votes of delegators anyway.
|
||||
|
||||
### Validator’s punishment for non-voting
|
||||
|
||||
Validators are required to vote on all proposals to ensure that results have legitimacy. Voting is part of validators' directives and failure to do it will result in a penalty.
|
||||
|
||||
If a validator’s address is not in the list of addresses that voted on a proposal and the vote is closed (i.e. `MinDeposit` was reached and `Voting period` is over), then the validator will automatically be partially slashed of `GovernancePenalty`.
|
||||
|
||||
*Note: Need to define values for `GovernancePenalty`*
|
||||
|
||||
**Exception:** If a proposal is a `Urgent` proposal and is accepted via the special condition of having a ratio of `Yes` votes to `InitTotalVotingPower` that exceeds 2:3, validators cannot be punished for not having voted on it. That is because the proposal will close as soon as the ratio exceeds 2:3, making it mechanically impossible for some validators to vote on it.
|
||||
|
||||
### Governance key and governance address
|
||||
|
||||
Validators can make use of a slot where they can designate a `Governance PubKey`. By default, a validator's `Governance PubKey` will be the same as its main PubKey. Validators can change this `Governance PubKey` by sending a `Change Governance PubKey` transaction signed by their main `Consensus PrivKey`. From there, they will be able to sign votes using the `Governance PrivKey` associated with their `Governance PubKey`. The `Governance PubKey` can be changed at any moment.
|
||||
|
||||
|
||||
## Software Upgrade
|
||||
|
||||
If proposals are of type `SoftwareUpgradeProposal`, then nodes need to upgrade their software to the new version that was voted. This process is divided in two steps.
|
||||
|
||||
### Signal
|
||||
|
||||
After a `SoftwareUpgradeProposal` is accepted, validators are expected to download and install the new version of the software while continuing to run the previous version. Once a validator has downloaded and installed the upgrade, it will start signaling to the network that it is ready to switch by including the proposal's `proposalID` in its *precommits*.(*Note: Confirmation that we want it in the precommit?*)
|
||||
|
||||
Note: There is only one signal slot per *precommit*. If several `SoftwareUpgradeProposals` are accepted in a short timeframe, a pipeline will form and they will be implemented one after the other in the order that they were accepted.
|
||||
|
||||
### Switch
|
||||
|
||||
Once a block contains more than 2/3rd *precommits* where a common `SoftwareUpgradeProposal` is signaled, all the nodes (including validator nodes, non-validating full nodes and light-nodes) are expected to switch to the new version of the software.
|
||||
|
||||
*Note: Not clear how the flip is handled programatically*
|
||||
|
||||
|
||||
## Implementation
|
||||
|
||||
*Disclaimer: This is a suggestion. Only structs and pseudocode. Actual logic and implementation might widely differ*
|
||||
|
||||
### State
|
||||
|
||||
#### Procedures
|
||||
|
||||
`Procedures` define the rule according to which votes are run. There can only be one active procedure at any given time. If governance wants to change a procedure, either to modify a value or add/remove a parameter, a new procedure has to be created and the previous one rendered inactive.
|
||||
|
||||
```Go
|
||||
type Procedure struct {
|
||||
VotingPeriod int64 // Length of the voting period. Initial value: 2 weeks
|
||||
MinDeposit int64 // Minimum deposit for a proposal to enter voting period.
|
||||
OptionSet []string // Options available to voters. {Yes, No, NoWithVeto, Abstain}
|
||||
ProposalTypes []string // Types available to submitters. {PlainTextProposal, SoftwareUpgradeProposal}
|
||||
Threshold rational.Rational // Minimum propotion of Yes votes for proposal to pass. Initial value: 0.5
|
||||
Veto rational.Rational // Minimum value of Veto votes to Total votes ratio for proposal to be vetoed. Initial value: 1/3
|
||||
MaxDepositPeriod int64 // Maximum period for Atom holders to deposit on a proposal. Initial value: 2 months
|
||||
GovernancePenalty int64 // Penalty if validator does not vote
|
||||
|
||||
IsActive bool // If true, procedure is active. Only one procedure can have isActive true.
|
||||
}
|
||||
```
|
||||
|
||||
**Store**:
|
||||
- `Procedures`: a mapping `map[int16]Procedure` of procedures indexed by their `ProcedureNumber`
|
||||
- `ActiveProcedureNumber`: returns current procedure number
|
||||
|
||||
#### Proposals
|
||||
|
||||
`Proposals` are item to be voted on.
|
||||
|
||||
```Go
|
||||
type Proposal struct {
|
||||
Title string // Title of the proposal
|
||||
Description string // Description of the proposal
|
||||
Type string // Type of proposal. Initial set {PlainTextProposal, SoftwareUpgradeProposal}
|
||||
Category bool // false=regular, true=urgent
|
||||
Deposit int64 // Current deposit on this proposal. Initial value is set at InitialDeposit
|
||||
SubmitBlock int64 // Height of the block where TxGovSubmitProposal was included
|
||||
|
||||
VotingStartBlock int64 // Height of the block where MinDeposit was reached. -1 if MinDeposit is not reached
|
||||
InitTotalVotingPower int64 // Total voting power when proposal enters voting period (default 0)
|
||||
InitProcedureNumber int16 // Procedure number of the active procedure when proposal enters voting period (default -1)
|
||||
Votes map[string]int64 // Votes for each option (Yes, No, NoWithVeto, Abstain)
|
||||
}
|
||||
```
|
||||
|
||||
We also introduce a type `ValidatorGovInfo`
|
||||
|
||||
```Go
|
||||
type ValidatorGovInfo struct {
|
||||
InitVotingPower int64 // Voting power of validator when proposal enters voting period
|
||||
Minus int64 // Minus of validator, used to compute validator's voting power
|
||||
}
|
||||
```
|
||||
|
||||
**Store:**
|
||||
|
||||
- `Proposals`: A mapping `map[int64]Proposal` of proposals indexed by their `proposalID`
|
||||
- `Deposits`: A mapping `map[[]byte]int64` of deposits indexed by `<proposalID>:<depositorPubKey>` as `[]byte`. Given a `proposalID` and a `PubKey`, returns deposit (`nil` if `PubKey` has not deposited on the proposal)
|
||||
- `Options`: A mapping `map[[]byte]string` of options indexed by `<proposalID>:<voterPubKey>:<validatorPubKey>` as `[]byte`. Given a `proposalID`, a `PubKey` and a validator's `PubKey`, returns option chosen by this `PubKey` for this validator (`nil` if `PubKey` has not voted under this validator)
|
||||
- `ValidatorGovInfos`: A mapping `map[[]byte]ValidatorGovInfo` of validator's governance infos indexed by `<proposalID>:<validatorGovPubKey>`. Returns `nil` if proposal has not entered voting period or if `PubKey` was not the governance public key of a validator when proposal entered voting period.
|
||||
|
||||
|
||||
#### Proposal Processing Queue
|
||||
|
||||
**Store:**
|
||||
- `ProposalProcessingQueue`: A queue `queue[proposalID]` containing all the `ProposalIDs` of proposals that reached `MinDeposit`. Each round, the oldest element of `ProposalProcessingQueue` is checked during `BeginBlock` to see if `CurrentBlock == VotingStartBlock + InitProcedure.VotingPeriod`. If it is, then the application checks if validators in `InitVotingPowerList` have voted and, if not, applies `GovernancePenalty`. After that proposal is ejected from `ProposalProcessingQueue` and the next element of the queue is evaluated. Note that if a proposal is urgent and accepted under the special condition, its `ProposalID` must be ejected from `ProposalProcessingQueue`.
|
||||
|
||||
And the pseudocode for the `ProposalProcessingQueue`:
|
||||
|
||||
```
|
||||
in BeginBlock do
|
||||
|
||||
checkProposal() // First call of the recursive function
|
||||
|
||||
|
||||
// Recursive function. First call in BeginBlock
|
||||
func checkProposal()
|
||||
if (ProposalProcessingQueue.Peek() == nil)
|
||||
return
|
||||
|
||||
else
|
||||
proposalID = ProposalProcessingQueue.Peek()
|
||||
proposal = load(store, Proposals, proposalID)
|
||||
initProcedure = load(store, Procedures, proposal.InitProcedureNumber)
|
||||
|
||||
if (proposal.Category AND proposal.Votes['Yes']/proposal.InitTotalVotingPower >= 2/3)
|
||||
|
||||
// proposal was urgent and accepted under the special condition
|
||||
// no punishment
|
||||
|
||||
ProposalProcessingQueue.pop()
|
||||
checkProposal()
|
||||
|
||||
else if (CurrentBlock == proposal.VotingStartBlock + initProcedure.VotingPeriod)
|
||||
|
||||
activeProcedure = load(store, Procedures, ActiveProcedureNumber)
|
||||
|
||||
for each validator in CurrentBondedValidators
|
||||
validatorGovInfo = load(store, ValidatorGovInfos, validator.GovPubKey)
|
||||
|
||||
if (validatorGovInfo.InitVotingPower != nil)
|
||||
// validator was bonded when vote started
|
||||
|
||||
validatorOption = load(store, Options, validator.GovPubKey)
|
||||
if (validatorOption == nil)
|
||||
// validator did not vote
|
||||
slash validator by activeProcedure.GovernancePenalty
|
||||
|
||||
ProposalProcessingQueue.pop()
|
||||
checkProposal()
|
||||
```
|
||||
|
||||
|
||||
### Transactions
|
||||
|
||||
#### Proposal Submission
|
||||
|
||||
Proposals can be submitted by any Atom holder via a `TxGovSubmitProposal` transaction.
|
||||
|
||||
```Go
|
||||
type TxGovSubmitProposal struct {
|
||||
Title string // Title of the proposal
|
||||
Description string // Description of the proposal
|
||||
Type string // Type of proposal. Initial set {PlainTextProposal, SoftwareUpgradeProposal}
|
||||
Category bool // false=regular, true=urgent
|
||||
InitialDeposit int64 // Initial deposit paid by sender. Must be strictly positive.
|
||||
}
|
||||
```
|
||||
|
||||
**State modifications:**
|
||||
- Generate new `proposalID`
|
||||
- Create new `Proposal`
|
||||
- Initialise `Proposals` attributes
|
||||
- Store sender's deposit in `Deposits`
|
||||
- Decrease balance of sender by `InitialDeposit`
|
||||
- If `MinDeposit` is reached:
|
||||
- Push `proposalID` in `ProposalProcessingQueueEnd`
|
||||
- Store each validator's voting power in `ValidatorGovInfos`
|
||||
|
||||
A `TxGovSubmitProposal` transaction can be handled according to the following pseudocode
|
||||
|
||||
```
|
||||
// PSEUDOCODE //
|
||||
// Check if TxGovSubmitProposal is valid. If it is, create proposal //
|
||||
|
||||
upon receiving txGovSubmitProposal from sender do
|
||||
|
||||
if !correctlyFormatted(txGovSubmitProposal) then
|
||||
// check if proposal is correctly formatted. Includes fee payment.
|
||||
|
||||
throw
|
||||
|
||||
else
|
||||
if (txGovSubmitProposal.InitialDeposit <= 0) OR (sender.AtomBalance < InitialDeposit) then
|
||||
// InitialDeposit is negative or null OR sender has insufficient funds
|
||||
|
||||
throw
|
||||
|
||||
else
|
||||
sender.AtomBalance -= txGovSubmitProposal.InitialDeposit
|
||||
|
||||
proposalID = generate new proposalID
|
||||
proposal = NewProposal()
|
||||
|
||||
proposal.Title = txGovSubmitProposal.Title
|
||||
proposal.Description = txGovSubmitProposal.Description
|
||||
proposal.Type = txGovSubmitProposal.Type
|
||||
proposal.Category = txGovSubmitProposal.Category
|
||||
proposal.Deposit = txGovSubmitProposal.InitialDeposit
|
||||
proposal.SubmitBlock = CurrentBlock
|
||||
|
||||
store(Deposits, <proposalID>:<sender>, txGovSubmitProposal.InitialDeposit)
|
||||
activeProcedure = load(store, Procedures, ActiveProcedureNumber)
|
||||
|
||||
if (txGovSubmitProposal.InitialDeposit < activeProcedure.MinDeposit) then
|
||||
// MinDeposit is not reached
|
||||
|
||||
proposal.VotingStartBlock = -1
|
||||
proposal.InitTotalVotingPower = 0
|
||||
proposal.InitProcedureNumber = -1
|
||||
|
||||
else
|
||||
// MinDeposit is reached
|
||||
|
||||
proposal.VotingStartBlock = CurrentBlock
|
||||
proposal.InitTotalVotingPower = TotalVotingPower
|
||||
proposal.InitProcedureNumber = ActiveProcedureNumber
|
||||
|
||||
for each validator in CurrentBondedValidators
|
||||
// Store voting power of each bonded validator
|
||||
|
||||
validatorGovInfo = NewValidatorGovInfo()
|
||||
validatorGovInfo.InitVotingPower = validator.VotingPower
|
||||
validatorGovInfo.Minus = 0
|
||||
|
||||
store(ValidatorGovInfos, <proposalID>:<validator.GovPubKey>, validatorGovInfo)
|
||||
|
||||
ProposalProcessingQueue.push(proposalID)
|
||||
|
||||
store(Proposals, proposalID, proposal) // Store proposal in Proposals mapping
|
||||
return proposalID
|
||||
```
|
||||
|
||||
|
||||
|
||||
#### Deposit
|
||||
|
||||
Once a proposal is submitted, if `Proposal.Deposit < ActiveProcedure.MinDeposit`, Atom holders can send `TxGovDeposit` transactions to increase the proposal's deposit.
|
||||
|
||||
```Go
|
||||
type TxGovDeposit struct {
|
||||
ProposalID int64 // ID of the proposal
|
||||
Deposit int64 // Number of Atoms to add to the proposal's deposit
|
||||
}
|
||||
```
|
||||
|
||||
**State modifications:**
|
||||
- Decrease balance of sender by `deposit`
|
||||
- Initialize or increase `deposit` of sender in `Deposits`
|
||||
- Increase `proposal.Deposit` by sender's `deposit`
|
||||
- If `MinDeposit` is reached:
|
||||
- Push `proposalID` in `ProposalProcessingQueueEnd`
|
||||
- Store each validator's voting power in `ValidatorGovInfos`
|
||||
|
||||
A `TxGovDeposit` transaction has to go through a number of checks to be valid. These checks are outlined in the following pseudocode.
|
||||
|
||||
```
|
||||
// PSEUDOCODE //
|
||||
// Check if TxGovDeposit is valid. If it is, increase deposit and check if MinDeposit is reached
|
||||
|
||||
upon receiving txGovDeposit from sender do
|
||||
// check if proposal is correctly formatted. Includes fee payment.
|
||||
|
||||
if !correctlyFormatted(txGovDeposit) then
|
||||
throw
|
||||
|
||||
else
|
||||
proposal = load(store, Proposals, txGovDeposit.ProposalID)
|
||||
|
||||
if (proposal == nil) then
|
||||
// There is no proposal for this proposalID
|
||||
|
||||
throw
|
||||
|
||||
else
|
||||
if (txGovDeposit.Deposit <= 0) OR (sender.AtomBalance < txGovDeposit.Deposit)
|
||||
// deposit is negative or null OR sender has insufficient funds
|
||||
|
||||
throw
|
||||
|
||||
else
|
||||
activeProcedure = load(store, Procedures, ActiveProcedureNumber)
|
||||
if (proposal.Deposit >= activeProcedure.MinDeposit) then
|
||||
// MinDeposit was reached
|
||||
|
||||
throw
|
||||
|
||||
else
|
||||
if (CurrentBlock >= proposal.SubmitBlock + activeProcedure.MaxDepositPeriod) then
|
||||
// Maximum deposit period reached
|
||||
|
||||
throw
|
||||
|
||||
else
|
||||
// sender can deposit
|
||||
|
||||
sender.AtomBalance -= txGovDeposit.Deposit
|
||||
deposit = load(store, Deposits, <txGovDeposit.ProposalID>:<sender>)
|
||||
|
||||
if (deposit == nil)
|
||||
// sender has never deposited on this proposal
|
||||
|
||||
store(Deposits, <txGovDeposit.ProposalID>:<sender>, deposit)
|
||||
|
||||
else
|
||||
// sender has already deposited on this proposal
|
||||
|
||||
newDeposit = deposit + txGovDeposit.Deposit
|
||||
store(Deposits, <txGovDeposit.ProposalID>:<sender>, newDeposit)
|
||||
|
||||
proposal.Deposit += txGovDeposit.Deposit
|
||||
|
||||
if (proposal.Deposit >= activeProcedure.MinDeposit) then
|
||||
// MinDeposit is reached, vote opens
|
||||
|
||||
proposal.VotingStartBlock = CurrentBlock
|
||||
proposal.InitTotalVotingPower = TotalVotingPower
|
||||
proposal.InitProcedureNumber = ActiveProcedureNumber
|
||||
|
||||
for each validator in CurrentBondedValidators
|
||||
// Store voting power of each bonded validator
|
||||
|
||||
validatorGovInfo = NewValidatorGovInfo()
|
||||
validatorGovInfo.InitVotingPower = validator.VotingPower
|
||||
validatorGovInfo.Minus = 0
|
||||
|
||||
store(ValidatorGovInfos, <proposalID>:<validator.GovPubKey>, validatorGovInfo)
|
||||
|
||||
ProposalProcessingQueue.push(txGovDeposit.ProposalID)
|
||||
```
|
||||
|
||||
#### Claiming deposit
|
||||
|
||||
Finally, if the proposal is accepted or `MinDeposit` was not reached before the end of the `MaximumDepositPeriod`, then Atom holders can send `TxGovClaimDeposit` transaction to claim their deposits.
|
||||
|
||||
```Go
|
||||
type TxGovClaimDeposit struct {
|
||||
ProposalID int64
|
||||
}
|
||||
```
|
||||
|
||||
**State modifications:**
|
||||
If conditions are met, reimburse the deposit, i.e.
|
||||
- Increase `AtomBalance` of sender by `deposit`
|
||||
- Set `deposit` of sender in `DepositorsList` to 0
|
||||
|
||||
And the associated pseudocode
|
||||
|
||||
```
|
||||
// PSEUDOCODE //
|
||||
/* Check if TxGovClaimDeposit is valid. If vote never started and MaxDepositPeriod is reached or if vote started and proposal was accepted, return deposit */
|
||||
|
||||
upon receiving txGovClaimDeposit from sender do
|
||||
// check if proposal is correctly formatted. Includes fee payment.
|
||||
|
||||
if !correctlyFormatted(txGovClaimDeposit) then
|
||||
throw
|
||||
|
||||
else
|
||||
proposal = load(store, Proposals, txGovDeposit.ProposalID)
|
||||
|
||||
if (proposal == nil) then
|
||||
// There is no proposal for this proposalID
|
||||
|
||||
throw
|
||||
|
||||
else
|
||||
deposit = load(store, Deposits, <txGovClaimDeposit.ProposalID>:<sender>)
|
||||
|
||||
if (deposit == nil)
|
||||
// sender has not deposited on this proposal
|
||||
|
||||
throw
|
||||
|
||||
else
|
||||
if (deposit <= 0)
|
||||
// deposit has already been claimed
|
||||
|
||||
throw
|
||||
|
||||
else
|
||||
if (proposal.VotingStartBlock <= 0)
|
||||
// Vote never started
|
||||
|
||||
activeProcedure = load(store, Procedures, ActiveProcedureNumber)
|
||||
if (CurrentBlock <= proposal.SubmitBlock + activeProcedure.MaxDepositPeriod)
|
||||
// MaxDepositPeriod is not reached
|
||||
|
||||
throw
|
||||
|
||||
else
|
||||
// MaxDepositPeriod is reached
|
||||
// Set sender's deposit to 0 and refund
|
||||
|
||||
store(Deposits, <txGovClaimDeposit.ProposalID>:<sender>, 0)
|
||||
sender.AtomBalance += deposit
|
||||
|
||||
else
|
||||
// Vote started
|
||||
|
||||
initProcedure = load(store, Procedures, proposal.InitProcedureNumber)
|
||||
|
||||
if (proposal.Category AND proposal.Votes['Yes']/proposal.InitTotalVotingPower >= 2/3) OR
|
||||
((CurrentBlock > proposal.VotingStartBlock + initProcedure.VotingPeriod) AND (proposal.Votes['NoWithVeto']/(proposal.Votes['Yes']+proposal.Votes['No']+proposal.Votes['NoWithVeto']) < 1/3) AND (proposal.Votes['Yes']/(proposal.Votes['Yes']+proposal.Votes['No']+proposal.Votes['NoWithVeto']) > 1/2)) then
|
||||
|
||||
// Proposal was accepted either because
|
||||
// Proposal was urgent and special condition was met
|
||||
// Voting period ended and vote satisfies threshold
|
||||
|
||||
store(Deposits, <txGovClaimDeposit.ProposalID>:<sender>, 0)
|
||||
sender.AtomBalance += deposit
|
||||
|
||||
```
|
||||
|
||||
|
||||
#### Vote
|
||||
|
||||
Once `ActiveProcedure.MinDeposit` is reached, voting period starts. From there, bonded Atom holders are able to send `TxGovVote` transactions to cast their vote on the proposal.
|
||||
|
||||
```Go
|
||||
type TxGovVote struct {
|
||||
ProposalID int64 // proposalID of the proposal
|
||||
Option string // option from OptionSet chosen by the voter
|
||||
ValidatorPubKey crypto.PubKey // PubKey of the validator voter wants to tie its vote to
|
||||
}
|
||||
```
|
||||
|
||||
**State modifications:**
|
||||
- If sender is not a validator and validator has not voted, initialize or increase minus of validator by sender's `voting power`
|
||||
- If sender is not a validator and validator has voted, decrease `proposal.Votes['validatorOption']` by sender's `voting power`
|
||||
- If sender is not a validator, increase `[proposal.Votes['txGovVote.Option']` by sender's `voting power`
|
||||
- If sender is a validator, increase `proposal.Votes['txGovVote.Option']` by validator's `InitialVotingPower - minus` (`minus` can be equal to 0)
|
||||
|
||||
Votes need to be tied to a validator in order to compute validator's voting power. If a delegator is bonded to multiple validators, it will have to send one transaction per validator (the UI should facilitate this so that multiple transactions can be sent in one "vote flow").
|
||||
If the sender is the validator itself, then it will input its own GovernancePubKey as `ValidatorPubKey`
|
||||
|
||||
|
||||
|
||||
Next is a pseudocode proposal of the way `TxGovVote` transactions can be handled:
|
||||
|
||||
```
|
||||
// PSEUDOCODE //
|
||||
// Check if TxGovVote is valid. If it is, count vote//
|
||||
|
||||
upon receiving txGovVote from sender do
|
||||
// check if proposal is correctly formatted. Includes fee payment.
|
||||
|
||||
if !correctlyFormatted(txGovDeposit) then
|
||||
throw
|
||||
|
||||
else
|
||||
proposal = load(store, Proposals, txGovDeposit.ProposalID)
|
||||
|
||||
if (proposal == nil) then
|
||||
// There is no proposal for this proposalID
|
||||
|
||||
throw
|
||||
|
||||
else
|
||||
initProcedure = load(store, Procedures, proposal.InitProcedureNumber) // get procedure that was active when vote opened
|
||||
validator = load(store, Validators, txGovVote.ValidatorPubKey)
|
||||
|
||||
if !initProcedure.OptionSet.includes(txGovVote.Option) OR
|
||||
(validator == nil) then
|
||||
|
||||
// Throws if
|
||||
// Option is not in Option Set of procedure that was active when vote opened OR if
|
||||
// ValidatorPubKey is not the GovPubKey of a current validator
|
||||
|
||||
throw
|
||||
|
||||
else
|
||||
option = load(store, Options, <txGovVote.ProposalID>:<sender>:<txGovVote.ValidatorPubKey>)
|
||||
|
||||
if (option != nil)
|
||||
// sender has already voted with the Atoms bonded to ValidatorPubKey
|
||||
|
||||
throw
|
||||
|
||||
else
|
||||
if (proposal.VotingStartBlock < 0) OR
|
||||
(CurrentBlock > proposal.VotingStartBlock + initProcedure.VotingPeriod) OR
|
||||
(proposal.VotingStartBlock < lastBondingBlock(sender, txGovVote.ValidatorPubKey) OR
|
||||
(proposal.VotingStartBlock < lastUnbondingBlock(sender, txGovVote.ValidatorPubKey) OR
|
||||
(proposal.Category AND proposal.Votes['Yes']/proposal.InitTotalVotingPower >= 2/3) then
|
||||
|
||||
// Throws if
|
||||
// Vote has not started OR if
|
||||
// Vote had ended OR if
|
||||
// sender bonded Atoms to ValidatorPubKey after start of vote OR if
|
||||
// sender unbonded Atoms from ValidatorPubKey after start of vote OR if
|
||||
// proposal is urgent and special condition is met, i.e. proposal is accepted and closed
|
||||
|
||||
throw
|
||||
|
||||
else
|
||||
validatorGovInfo = load(store, ValidatorGovInfos, <txGovVote.ProposalID>:<validator.ValidatorGovPubKey>)
|
||||
|
||||
if (validatorGovInfo == nil)
|
||||
// validator became validator after proposal entered voting period
|
||||
|
||||
throw
|
||||
|
||||
else
|
||||
// sender can vote, check if sender == validator and store sender's option in Options
|
||||
|
||||
store(Options, <txGovVote.ProposalID>:<sender>:<txGovVote.ValidatorPubKey>, txGovVote.Option)
|
||||
|
||||
if (sender != validator.GovPubKey)
|
||||
// Here, sender is not the Governance PubKey of the validator whose PubKey is txGovVote.ValidatorPubKey
|
||||
|
||||
if sender does not have bonded Atoms to txGovVote.ValidatorPubKey then
|
||||
// check in Staking module
|
||||
|
||||
throw
|
||||
|
||||
else
|
||||
validatorOption = load(store, Options, <txGovVote.ProposalID>:<sender>:<txGovVote.ValidatorPubKey)
|
||||
|
||||
if (validatorOption == nil)
|
||||
// Validator has not voted already
|
||||
|
||||
validatorGovInfo.Minus += sender.bondedAmounTo(txGovVote.ValidatorPubKey)
|
||||
store(ValidatorGovInfos, <txGovVote.ProposalID>:<validator.ValidatorGovPubKey>, validatorGovInfo)
|
||||
|
||||
else
|
||||
// Validator has already voted
|
||||
// Reduce votes of option chosen by validator by sender's bonded Amount
|
||||
|
||||
proposal.Votes['validatorOption'] -= sender.bondedAmountTo(txGovVote.ValidatorPubKey)
|
||||
|
||||
// increase votes of option chosen by sender by bonded Amount
|
||||
proposal.Votes['txGovVote.Option'] += sender.bondedAmountTo(txGovVote.ValidatorPubKey)
|
||||
|
||||
else
|
||||
// sender is the Governance PubKey of the validator whose main PubKey is txGovVote.ValidatorPubKey
|
||||
// i.e. sender == validator
|
||||
|
||||
proposal.Votes['txGovVote.Option'] += (validatorGovInfo.InitVotingPower - validatorGovInfo.Minus)
|
||||
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Future improvements (not in scope for MVP)
|
||||
|
||||
The current documentation only describes the minimum viable product for the governance module. Future improvements may include:
|
||||
|
||||
- **`BountyProposals`:** If accepted, a `BountyProposal` creates an open bounty. The `BountyProposal` specifies how many Atoms will be given upon completion. These Atoms will be taken from the `reserve pool`. After a `BountyProposal` is accepted by governance, anybody can submit a `SoftwareUpgradeProposal` with the code to claim the bounty. Note that once a `BountyProposal` is accepted, the corresponding funds in the `reserve pool` are locked so that payment can always be honored. In order to link a `SoftwareUpgradeProposal` to an open bounty, the submitter of the `SoftwareUpgradeProposal` will use the `Proposal.LinkedProposal` attribute. If a `SoftwareUpgradeProposal` linked to an open bounty is accepted by governance, the funds that were reserved are automatically transferred to the submitter.
|
||||
- **Complex delegation:** Delegators could choose other representatives than their validators. Ultimately, the chain of representatives would always end up to a validator, but delegators could inherit the vote of their chosen representative before they inherit the vote of their validator. In other words, they would only inherit the vote of their validator if their other appointed representative did not vote.
|
||||
- **`ParameterProposals` and `WhitelistProposals`:** These proposals would automatically change pre-defined parameters and whitelists. Upon acceptance, these proposals would not require validators to do the signal and switch process.
|
||||
- **Better process for proposal review:** There would be two parts to `proposal.Deposit`, one for anti-spam (same as in MVP) and an other one to reward third party auditors.
|
|
@ -29,6 +29,10 @@ var (
|
|||
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)},
|
||||
|
@ -82,8 +86,8 @@ func TestMsgs(t *testing.T) {
|
|||
|
||||
sequences := []int64{0}
|
||||
for i, m := range msgs {
|
||||
sig := priv1.Sign(sdk.StdSignBytes(chainID, sequences, m.msg))
|
||||
tx := sdk.NewStdTx(m.msg, []sdk.StdSignature{{
|
||||
sig := priv1.Sign(sdk.StdSignBytes(chainID, sequences, fee, m.msg))
|
||||
tx := sdk.NewStdTx(m.msg, fee, []sdk.StdSignature{{
|
||||
PubKey: priv1.PubKey(),
|
||||
Signature: sig,
|
||||
}})
|
||||
|
@ -180,8 +184,8 @@ func TestSendMsgWithAccounts(t *testing.T) {
|
|||
|
||||
// Sign the tx
|
||||
sequences := []int64{0}
|
||||
sig := priv1.Sign(sdk.StdSignBytes(chainID, sequences, sendMsg))
|
||||
tx := sdk.NewStdTx(sendMsg, []sdk.StdSignature{{
|
||||
sig := priv1.Sign(sdk.StdSignBytes(chainID, sequences, fee, sendMsg))
|
||||
tx := sdk.NewStdTx(sendMsg, fee, []sdk.StdSignature{{
|
||||
PubKey: priv1.PubKey(),
|
||||
Signature: sig,
|
||||
}})
|
||||
|
@ -213,7 +217,7 @@ func TestSendMsgWithAccounts(t *testing.T) {
|
|||
|
||||
// resigning the tx with the bumped sequence should work
|
||||
sequences = []int64{1}
|
||||
sig = priv1.Sign(sdk.StdSignBytes(chainID, sequences, tx.Msg))
|
||||
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)
|
||||
|
@ -270,9 +274,9 @@ func TestQuizMsg(t *testing.T) {
|
|||
func SignCheckDeliver(t *testing.T, bapp *BasecoinApp, msg sdk.Msg, seq int64, expPass bool) {
|
||||
|
||||
// Sign the tx
|
||||
tx := sdk.NewStdTx(msg, []sdk.StdSignature{{
|
||||
tx := sdk.NewStdTx(msg, fee, []sdk.StdSignature{{
|
||||
PubKey: priv1.PubKey(),
|
||||
Signature: priv1.Sign(sdk.StdSignBytes(chainID, []int64{seq}, msg)),
|
||||
Signature: priv1.Sign(sdk.StdSignBytes(chainID, []int64{seq}, fee, msg)),
|
||||
Sequence: seq,
|
||||
}})
|
||||
|
||||
|
|
|
@ -13,13 +13,14 @@ import (
|
|||
"github.com/cosmos/cosmos-sdk/client/lcd"
|
||||
"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"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/examples/basecoin/app"
|
||||
"github.com/cosmos/cosmos-sdk/examples/basecoin/types"
|
||||
coolcmd "github.com/cosmos/cosmos-sdk/examples/basecoin/x/cool/commands"
|
||||
)
|
||||
|
||||
// gaiacliCmd is the entry point for this binary
|
||||
|
@ -41,6 +42,10 @@ func main() {
|
|||
// 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(basecliCmd)
|
||||
basecliCmd.AddCommand(client.LineBreak)
|
||||
|
@ -68,7 +73,7 @@ func main() {
|
|||
// add proxy, version and key info
|
||||
basecliCmd.AddCommand(
|
||||
client.LineBreak,
|
||||
lcd.ServeCommand(),
|
||||
lcd.ServeCommand(cdc),
|
||||
keys.Commands(),
|
||||
client.LineBreak,
|
||||
version.VersionCmd,
|
||||
|
|
|
@ -59,6 +59,7 @@ func generateApp(rootDir string, logger log.Logger) (abci.Application, error) {
|
|||
}
|
||||
|
||||
func main() {
|
||||
// TODO: set logger through CLI
|
||||
logger := log.NewTMLogger(log.NewSyncWriter(os.Stdout)).
|
||||
With("module", "main")
|
||||
|
||||
|
|
|
@ -5,7 +5,9 @@ import (
|
|||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/client"
|
||||
"github.com/cosmos/cosmos-sdk/client/builder"
|
||||
"github.com/cosmos/cosmos-sdk/wire"
|
||||
|
||||
|
@ -31,8 +33,19 @@ func QuizTxCmd(cdc *wire.Codec) *cobra.Command {
|
|||
// create the message
|
||||
msg := cool.NewQuizMsg(from, args[0])
|
||||
|
||||
// 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(msg, cdc)
|
||||
res, err := builder.SignBuildBroadcast(name, passphrase, msg, cdc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -59,11 +72,22 @@ func SetTrendTxCmd(cdc *wire.Codec) *cobra.Command {
|
|||
return err
|
||||
}
|
||||
|
||||
// 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(msg, cdc)
|
||||
res, err := builder.SignBuildBroadcast(name, passphrase, msg, cdc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -37,7 +37,7 @@ func (msg SetTrendMsg) String() string {
|
|||
// Validate Basic is used to quickly disqualify obviously invalid messages quickly
|
||||
func (msg SetTrendMsg) ValidateBasic() sdk.Error {
|
||||
if len(msg.Sender) == 0 {
|
||||
return sdk.ErrUnrecognizedAddress(msg.Sender).Trace("")
|
||||
return sdk.ErrUnrecognizedAddress(msg.Sender.String()).Trace("")
|
||||
}
|
||||
if strings.Contains(msg.Cool, "hot") {
|
||||
return sdk.ErrUnauthorized("").Trace("hot is not cool")
|
||||
|
@ -88,7 +88,7 @@ func (msg QuizMsg) String() string {
|
|||
// Validate Basic is used to quickly disqualify obviously invalid messages quickly
|
||||
func (msg QuizMsg) ValidateBasic() sdk.Error {
|
||||
if len(msg.Sender) == 0 {
|
||||
return sdk.ErrUnrecognizedAddress(msg.Sender).Trace("")
|
||||
return sdk.ErrUnrecognizedAddress(msg.Sender.String()).Trace("")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -51,10 +51,6 @@ func (tx kvstoreTx) GetSignatures() []sdk.StdSignature {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (tx kvstoreTx) GetFeePayer() sdk.Address {
|
||||
return nil
|
||||
}
|
||||
|
||||
// takes raw transaction bytes and decodes them into an sdk.Tx. An sdk.Tx has
|
||||
// all the signatures and can be used to authenticate.
|
||||
func decodeTx(txBytes []byte) (sdk.Tx, sdk.Error) {
|
||||
|
|
|
@ -64,10 +64,6 @@ func (tx kvstoreTx) GetSignatures() []sdk.StdSignature {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (tx kvstoreTx) GetFeePayer() sdk.Address {
|
||||
return nil
|
||||
}
|
||||
|
||||
// takes raw transaction bytes and decodes them into an sdk.Tx. An sdk.Tx has
|
||||
// all the signatures and can be used to authenticate.
|
||||
func decodeTx(txBytes []byte) (sdk.Tx, sdk.Error) {
|
||||
|
|
|
@ -2,33 +2,27 @@ package server
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/tendermint/go-crypto/keys"
|
||||
"github.com/tendermint/go-crypto/keys/words"
|
||||
cmn "github.com/tendermint/tmlibs/common"
|
||||
dbm "github.com/tendermint/tmlibs/db"
|
||||
"github.com/tendermint/tmlibs/log"
|
||||
|
||||
tcmd "github.com/tendermint/tendermint/cmd/tendermint/commands"
|
||||
cfg "github.com/tendermint/tendermint/config"
|
||||
tmtypes "github.com/tendermint/tendermint/types"
|
||||
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
)
|
||||
|
||||
// InitCmd will initialize all files for tendermint,
|
||||
// along with proper app_options.
|
||||
// along with proper app_state.
|
||||
// The application can pass in a function to generate
|
||||
// proper options. And may want to use GenerateCoinKey
|
||||
// proper state. And may want to use GenerateCoinKey
|
||||
// to create default account(s).
|
||||
func InitCmd(gen GenOptions, logger log.Logger) *cobra.Command {
|
||||
func InitCmd(gen GenAppState, logger log.Logger) *cobra.Command {
|
||||
cmd := initCmd{
|
||||
gen: gen,
|
||||
logger: logger,
|
||||
genAppState: gen,
|
||||
logger: logger,
|
||||
}
|
||||
return &cobra.Command{
|
||||
Use: "init",
|
||||
|
@ -37,39 +31,14 @@ func InitCmd(gen GenOptions, logger log.Logger) *cobra.Command {
|
|||
}
|
||||
}
|
||||
|
||||
// GenOptions can parse command-line and flag to
|
||||
// generate default app_options for the genesis file.
|
||||
// GenAppState can parse command-line and flag to
|
||||
// generate default app_state for the genesis file.
|
||||
// This is application-specific
|
||||
type GenOptions func(args []string) (json.RawMessage, error)
|
||||
|
||||
// GenerateCoinKey returns the address of a public key,
|
||||
// along with the secret phrase to recover the private key.
|
||||
// You can give coins to this address and return the recovery
|
||||
// phrase to the user to access them.
|
||||
func GenerateCoinKey() (sdk.Address, string, error) {
|
||||
// construct an in-memory key store
|
||||
codec, err := words.LoadCodec("english")
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
keybase := keys.New(
|
||||
dbm.NewMemDB(),
|
||||
codec,
|
||||
)
|
||||
|
||||
// generate a private key, with recovery phrase
|
||||
info, secret, err := keybase.Create("name", "pass", keys.AlgoEd25519)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
addr := info.PubKey.Address()
|
||||
return addr, secret, nil
|
||||
}
|
||||
type GenAppState func(args []string) (json.RawMessage, error)
|
||||
|
||||
type initCmd struct {
|
||||
gen GenOptions
|
||||
logger log.Logger
|
||||
genAppState GenAppState
|
||||
logger log.Logger
|
||||
}
|
||||
|
||||
func (c initCmd) run(cmd *cobra.Command, args []string) error {
|
||||
|
@ -85,19 +54,19 @@ func (c initCmd) run(cmd *cobra.Command, args []string) error {
|
|||
}
|
||||
|
||||
// no app_options, leave like tendermint
|
||||
if c.gen == nil {
|
||||
if c.genAppState == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Now, we want to add the custom app_options
|
||||
options, err := c.gen(args)
|
||||
// Now, we want to add the custom app_state
|
||||
appState, err := c.genAppState(args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// And add them to the genesis file
|
||||
genFile := config.GenesisFile()
|
||||
return addGenesisOptions(genFile, options)
|
||||
return addGenesisState(genFile, appState)
|
||||
}
|
||||
|
||||
// This was copied from tendermint/cmd/tendermint/commands/init.go
|
||||
|
@ -141,7 +110,7 @@ func (c initCmd) initTendermintFiles(config *cfg.Config) error {
|
|||
// so we can add one line.
|
||||
type GenesisDoc map[string]json.RawMessage
|
||||
|
||||
func addGenesisOptions(filename string, options json.RawMessage) error {
|
||||
func addGenesisState(filename string, appState json.RawMessage) error {
|
||||
bz, err := ioutil.ReadFile(filename)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -153,7 +122,7 @@ func addGenesisOptions(filename string, options json.RawMessage) error {
|
|||
return err
|
||||
}
|
||||
|
||||
doc["app_state"] = options
|
||||
doc["app_state"] = appState
|
||||
out, err := json.MarshalIndent(doc, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -161,23 +130,3 @@ func addGenesisOptions(filename string, options json.RawMessage) error {
|
|||
|
||||
return ioutil.WriteFile(filename, out, 0600)
|
||||
}
|
||||
|
||||
// GetGenesisJSON returns a new tendermint genesis with Basecoin app_options
|
||||
// that grant a large amount of "mycoin" to a single address
|
||||
// TODO: A better UX for generating genesis files
|
||||
func GetGenesisJSON(pubkey, chainID, denom, addr string, options string) string {
|
||||
return fmt.Sprintf(`{
|
||||
"accounts": [{
|
||||
"address": "%s",
|
||||
"coins": [
|
||||
{
|
||||
"denom": "%s",
|
||||
"amount": 9007199254740992
|
||||
}
|
||||
]
|
||||
}],
|
||||
"plugin_options": [
|
||||
"coin/issuer", {"app": "sigs", "addr": "%s"}%s
|
||||
]
|
||||
}`, addr, denom, addr, options)
|
||||
}
|
||||
|
|
|
@ -1,11 +1,8 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/tendermint/tmlibs/log"
|
||||
|
@ -13,21 +10,8 @@ import (
|
|||
"github.com/cosmos/cosmos-sdk/mock"
|
||||
)
|
||||
|
||||
// setupViper creates a homedir to run inside,
|
||||
// and returns a cleanup function to defer
|
||||
func setupViper() func() {
|
||||
rootDir, err := ioutil.TempDir("", "mock-sdk-cmd")
|
||||
if err != nil {
|
||||
panic(err) // fuck it!
|
||||
}
|
||||
viper.Set("home", rootDir)
|
||||
return func() {
|
||||
os.RemoveAll(rootDir)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInit(t *testing.T) {
|
||||
defer setupViper()()
|
||||
defer setupViper(t)()
|
||||
|
||||
logger := log.NewNopLogger()
|
||||
cmd := InitCmd(mock.GenInitOptions, logger)
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"github.com/tendermint/go-crypto/keys"
|
||||
"github.com/tendermint/go-crypto/keys/words"
|
||||
dbm "github.com/tendermint/tmlibs/db"
|
||||
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
)
|
||||
|
||||
// GenerateCoinKey returns the address of a public key,
|
||||
// along with the secret phrase to recover the private key.
|
||||
// You can give coins to this address and return the recovery
|
||||
// phrase to the user to access them.
|
||||
func GenerateCoinKey() (sdk.Address, string, error) {
|
||||
// construct an in-memory key store
|
||||
codec, err := words.LoadCodec("english")
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
keybase := keys.New(
|
||||
dbm.NewMemDB(),
|
||||
codec,
|
||||
)
|
||||
|
||||
// generate a private key, with recovery phrase
|
||||
info, secret, err := keybase.Create("name", "pass", keys.AlgoEd25519)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
addr := info.PubKey.Address()
|
||||
return addr, secret, nil
|
||||
}
|
|
@ -23,14 +23,14 @@ const (
|
|||
|
||||
// appGenerator lets us lazily initialize app, using home dir
|
||||
// and other flags (?) to start
|
||||
type appGenerator func(string, log.Logger) (abci.Application, error)
|
||||
type appCreator func(string, log.Logger) (abci.Application, error)
|
||||
|
||||
// StartCmd runs the service passed in, either
|
||||
// stand-alone, or in-process with tendermint
|
||||
func StartCmd(app appGenerator, logger log.Logger) *cobra.Command {
|
||||
func StartCmd(app appCreator, logger log.Logger) *cobra.Command {
|
||||
start := startCmd{
|
||||
app: app,
|
||||
logger: logger,
|
||||
appCreator: app,
|
||||
logger: logger,
|
||||
}
|
||||
cmd := &cobra.Command{
|
||||
Use: "start",
|
||||
|
@ -48,8 +48,8 @@ func StartCmd(app appGenerator, logger log.Logger) *cobra.Command {
|
|||
}
|
||||
|
||||
type startCmd struct {
|
||||
app appGenerator
|
||||
logger log.Logger
|
||||
appCreator appCreator
|
||||
logger log.Logger
|
||||
}
|
||||
|
||||
func (s startCmd) run(cmd *cobra.Command, args []string) error {
|
||||
|
@ -65,7 +65,7 @@ func (s startCmd) startStandAlone() error {
|
|||
// Generate the app in the proper dir
|
||||
addr := viper.GetString(flagAddress)
|
||||
home := viper.GetString("home")
|
||||
app, err := s.app(home, s.logger)
|
||||
app, err := s.appCreator(home, s.logger)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -92,7 +92,7 @@ func (s startCmd) startInProcess() error {
|
|||
}
|
||||
|
||||
home := cfg.RootDir
|
||||
app, err := s.app(home, s.logger)
|
||||
app, err := s.appCreator(home, s.logger)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -1,12 +1,10 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
|
@ -15,7 +13,7 @@ import (
|
|||
)
|
||||
|
||||
func TestStartStandAlone(t *testing.T) {
|
||||
defer setupViper()()
|
||||
defer setupViper(t)()
|
||||
|
||||
logger := log.NewNopLogger()
|
||||
initCmd := InitCmd(mock.GenInitOptions, logger)
|
||||
|
@ -26,14 +24,14 @@ func TestStartStandAlone(t *testing.T) {
|
|||
viper.Set(flagWithTendermint, false)
|
||||
viper.Set(flagAddress, "localhost:11122")
|
||||
startCmd := StartCmd(mock.NewApp, logger)
|
||||
startCmd.Flags().Set(flagAddress, FreeTCPAddr(t)) // set to a new free address
|
||||
timeout := time.Duration(3) * time.Second
|
||||
|
||||
err = runOrTimeout(startCmd, timeout)
|
||||
require.NoError(t, err)
|
||||
RunOrTimeout(startCmd, timeout, t)
|
||||
}
|
||||
|
||||
func TestStartWithTendermint(t *testing.T) {
|
||||
defer setupViper()()
|
||||
defer setupViper(t)()
|
||||
|
||||
logger := log.NewTMLogger(log.NewSyncWriter(os.Stdout)).
|
||||
With("module", "mock-cmd")
|
||||
|
@ -45,28 +43,11 @@ func TestStartWithTendermint(t *testing.T) {
|
|||
// set up app and start up
|
||||
viper.Set(flagWithTendermint, true)
|
||||
startCmd := StartCmd(mock.NewApp, logger)
|
||||
startCmd.Flags().Set(flagAddress, FreeTCPAddr(t)) // set to a new free address
|
||||
timeout := time.Duration(3) * time.Second
|
||||
|
||||
err = runOrTimeout(startCmd, timeout)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
//a, _ := startCmd.Flags().GetString(flagAddress)
|
||||
//panic(a)
|
||||
|
||||
func runOrTimeout(cmd *cobra.Command, timeout time.Duration) error {
|
||||
done := make(chan error)
|
||||
go func(out chan<- error) {
|
||||
// this should NOT exit
|
||||
err := cmd.RunE(nil, nil)
|
||||
if err != nil {
|
||||
out <- err
|
||||
}
|
||||
out <- fmt.Errorf("start died for unknown reasons")
|
||||
}(done)
|
||||
timer := time.NewTimer(timeout)
|
||||
|
||||
select {
|
||||
case err := <-done:
|
||||
return err
|
||||
case <-timer.C:
|
||||
return nil
|
||||
}
|
||||
RunOrTimeout(startCmd, timeout, t)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,82 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/mock"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/tendermint/tmlibs/cli"
|
||||
"github.com/tendermint/tmlibs/log"
|
||||
)
|
||||
|
||||
// Get a free address for a test tendermint server
|
||||
// protocol is either tcp, http, etc
|
||||
func FreeTCPAddr(t *testing.T) string {
|
||||
l, err := net.Listen("tcp", "0.0.0.0:0")
|
||||
defer l.Close()
|
||||
require.Nil(t, err)
|
||||
|
||||
port := l.Addr().(*net.TCPAddr).Port
|
||||
addr := fmt.Sprintf("tcp://0.0.0.0:%d", port)
|
||||
return addr
|
||||
}
|
||||
|
||||
// setupViper creates a homedir to run inside,
|
||||
// and returns a cleanup function to defer
|
||||
func setupViper(t *testing.T) func() {
|
||||
rootDir, err := ioutil.TempDir("", "mock-sdk-cmd")
|
||||
require.Nil(t, err)
|
||||
viper.Set(cli.HomeFlag, rootDir)
|
||||
return func() {
|
||||
os.RemoveAll(rootDir)
|
||||
}
|
||||
}
|
||||
|
||||
// Begin the server pass up the channel to close
|
||||
// NOTE pass up the channel so it can be closed at the end of the process
|
||||
func StartServer(t *testing.T) chan error {
|
||||
defer setupViper(t)()
|
||||
|
||||
// init server
|
||||
initCmd := InitCmd(mock.GenInitOptions, log.NewNopLogger())
|
||||
err := initCmd.RunE(nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
// start server
|
||||
viper.Set(flagWithTendermint, true)
|
||||
startCmd := StartCmd(mock.NewApp, log.NewNopLogger())
|
||||
startCmd.Flags().Set(flagAddress, FreeTCPAddr(t)) // set to a new free address
|
||||
startCmd.Flags().Set("rpc.laddr", FreeTCPAddr(t)) // set to a new free address
|
||||
timeout := time.Duration(3) * time.Second
|
||||
|
||||
return RunOrTimeout(startCmd, timeout, t)
|
||||
}
|
||||
|
||||
// Run or Timout RunE of command passed in
|
||||
func RunOrTimeout(cmd *cobra.Command, timeout time.Duration, t *testing.T) chan error {
|
||||
done := make(chan error)
|
||||
go func(out chan<- error) {
|
||||
// this should NOT exit
|
||||
err := cmd.RunE(nil, nil)
|
||||
if err != nil {
|
||||
out <- err
|
||||
}
|
||||
out <- fmt.Errorf("start died for unknown reasons")
|
||||
}(done)
|
||||
timer := time.NewTimer(timeout)
|
||||
|
||||
select {
|
||||
case err := <-done:
|
||||
require.NoError(t, err)
|
||||
case <-timer.C:
|
||||
return done
|
||||
}
|
||||
return done
|
||||
}
|
|
@ -0,0 +1,320 @@
|
|||
package tests
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
//"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/server"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// Tests assume the `basecoind` and `basecli` binaries
|
||||
// have been built and are located in `./build`
|
||||
|
||||
// TODO remove test dirs if tests are successful
|
||||
|
||||
//nolint
|
||||
var (
|
||||
basecoind = "build/basecoind"
|
||||
basecli = "build/basecli"
|
||||
|
||||
basecoindDir = "./tmp-basecoind-tests"
|
||||
basecliDir = "./tmp-basecli-tests"
|
||||
|
||||
ACCOUNTS = []string{"alice", "bob", "charlie", "igor"}
|
||||
alice = ACCOUNTS[0]
|
||||
bob = ACCOUNTS[1]
|
||||
charlie = ACCOUNTS[2]
|
||||
igor = ACCOUNTS[3]
|
||||
)
|
||||
|
||||
func gopath() string {
|
||||
return filepath.Join(os.Getenv("GOPATH"), "src", "github.com", "cosmos", "cosmos-sdk")
|
||||
}
|
||||
|
||||
func whereIsBasecoind() string {
|
||||
return filepath.Join(gopath(), basecoind)
|
||||
}
|
||||
|
||||
func whereIsBasecli() string {
|
||||
return filepath.Join(gopath(), basecli)
|
||||
}
|
||||
|
||||
// Init Basecoin Test
|
||||
func TestInitBasecoin(t *testing.T, home string) string {
|
||||
var err error
|
||||
|
||||
password := "some-random-password"
|
||||
|
||||
initBasecoind := exec.Command(whereIsBasecoind(), "init", "--home", home)
|
||||
cmdWriter, err := initBasecoind.StdinPipe()
|
||||
require.Nil(t, err)
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
initBasecoind.Stdout = buf
|
||||
|
||||
if err = initBasecoind.Start(); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
_, err = cmdWriter.Write([]byte(password))
|
||||
require.Nil(t, err)
|
||||
cmdWriter.Close()
|
||||
|
||||
if err = initBasecoind.Wait(); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
// get seed from initialization
|
||||
theOutput := strings.Split(buf.String(), "\n")
|
||||
var seedLine int
|
||||
for _seedLine, o := range theOutput {
|
||||
if strings.HasPrefix(string(o), "Secret phrase") {
|
||||
seedLine = _seedLine + 1
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
seed := string(theOutput[seedLine])
|
||||
|
||||
// enable indexing
|
||||
err = appendToFile(path.Join(home, "config", "config.toml"), "\n\n[tx_indexing]\nindex_all_tags = true\n")
|
||||
require.Nil(t, err)
|
||||
|
||||
return seed
|
||||
}
|
||||
|
||||
func appendToFile(path string, text string) error {
|
||||
f, err := os.OpenFile(path, os.O_APPEND|os.O_WRONLY, 0600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer f.Close()
|
||||
|
||||
if _, err = f.WriteString(text); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func makeKeys() error {
|
||||
for _, acc := range ACCOUNTS {
|
||||
makeKeys := exec.Command(whereIsBasecli(), "keys", "add", acc, "--home", basecliDir)
|
||||
cmdWriter, err := makeKeys.StdinPipe()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
makeKeys.Stdout = os.Stdout
|
||||
if err := makeKeys.Start(); err != nil {
|
||||
return err
|
||||
}
|
||||
cmdWriter.Write([]byte("1234567890"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cmdWriter.Close()
|
||||
|
||||
if err := makeKeys.Wait(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func _TestSendCoins(t *testing.T) {
|
||||
if err := StartServer(); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
// send some coins
|
||||
// [zr] where dafuq do I get a FROM (oh, use --name)
|
||||
|
||||
sendTo := fmt.Sprintf("--to=%s", bob)
|
||||
sendFrom := fmt.Sprintf("--from=%s", alice)
|
||||
|
||||
cmdOut, err := exec.Command(whereIsBasecli(), "send", sendTo, "--amount=1000mycoin", sendFrom, "--seq=0").Output()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
fmt.Printf("sent: %s", string(cmdOut))
|
||||
|
||||
}
|
||||
|
||||
// expects TestInitBaseCoin to have been run
|
||||
func StartServer() error {
|
||||
// straight outta https://nathanleclaire.com/blog/2014/12/29/shelled-out-commands-in-golang/
|
||||
cmdName := whereIsBasecoind()
|
||||
cmdArgs := []string{"start", "--home", basecoindDir}
|
||||
|
||||
cmd := exec.Command(cmdName, cmdArgs...)
|
||||
cmdReader, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
scanner := bufio.NewScanner(cmdReader)
|
||||
go func() {
|
||||
for scanner.Scan() {
|
||||
fmt.Printf("running [basecoind start] %s\n", scanner.Text())
|
||||
}
|
||||
}()
|
||||
|
||||
err = cmd.Start()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = cmd.Wait()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
time.Sleep(5 * time.Second)
|
||||
|
||||
return nil
|
||||
|
||||
// TODO return cmd.Process so that we can later do something like:
|
||||
// cmd.Process.Kill()
|
||||
// see: https://stackoverflow.com/questions/11886531/terminating-a-process-started-with-os-exec-in-golang
|
||||
}
|
||||
|
||||
// Init Basecoin Test
|
||||
func InitServerForTest(t *testing.T) {
|
||||
Clean()
|
||||
|
||||
var err error
|
||||
|
||||
password := "some-random-password"
|
||||
usePassword := exec.Command("echo", password)
|
||||
|
||||
initBasecoind := exec.Command(whereIsBasecoind(), "init", "--home", basecoindDir)
|
||||
|
||||
initBasecoind.Stdin, err = usePassword.StdoutPipe()
|
||||
require.Nil(t, err)
|
||||
|
||||
initBasecoind.Stdout = os.Stdout
|
||||
|
||||
err = initBasecoind.Start()
|
||||
require.Nil(t, err)
|
||||
err = usePassword.Run()
|
||||
require.Nil(t, err)
|
||||
err = initBasecoind.Wait()
|
||||
require.Nil(t, err)
|
||||
|
||||
err = makeKeys()
|
||||
require.Nil(t, err)
|
||||
}
|
||||
|
||||
// expects TestInitBaseCoin to have been run
|
||||
func StartNodeServerForTest(t *testing.T, home string) *exec.Cmd {
|
||||
cmdName := whereIsBasecoind()
|
||||
cmdArgs := []string{"start", "--home", home}
|
||||
cmd := exec.Command(cmdName, cmdArgs...)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
err := cmd.Start()
|
||||
require.Nil(t, err)
|
||||
|
||||
// FIXME: if there is a nondeterministic node start failure,
|
||||
// we should probably make this read the logs to wait for RPC
|
||||
time.Sleep(time.Second * 2)
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
// expects TestInitBaseCoin to have been run
|
||||
func StartLCDServerForTest(t *testing.T, home, chainID string) (cmd *exec.Cmd, port string) {
|
||||
cmdName := whereIsBasecli()
|
||||
port = strings.Split(server.FreeTCPAddr(t), ":")[2]
|
||||
cmdArgs := []string{
|
||||
"rest-server",
|
||||
"--home",
|
||||
home,
|
||||
"--bind",
|
||||
fmt.Sprintf("localhost:%s", port),
|
||||
"--chain-id",
|
||||
chainID,
|
||||
}
|
||||
cmd = exec.Command(cmdName, cmdArgs...)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
err := cmd.Start()
|
||||
require.Nil(t, err)
|
||||
time.Sleep(time.Second * 2) // TODO: LOL
|
||||
return cmd, port
|
||||
}
|
||||
|
||||
// clean the directories
|
||||
func Clean() {
|
||||
// ignore errors b/c the dirs may not yet exist
|
||||
err := os.Remove(basecoindDir)
|
||||
panic(err)
|
||||
err = os.Remove(basecliDir)
|
||||
panic(err)
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
chainID = "staking_test"
|
||||
testDir = "./tmp_tests"
|
||||
)
|
||||
|
||||
func runTests() {
|
||||
|
||||
if err := os.Mkdir(testDir, 0666); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer os.Remove(testDir)
|
||||
|
||||
// make some keys
|
||||
|
||||
//if err := makeKeys(); err != nil {
|
||||
// panic(err)
|
||||
//}
|
||||
|
||||
if err := initServer(); err != nil {
|
||||
fmt.Printf("Err: %v", err)
|
||||
panic(err)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func initServer() error {
|
||||
serveDir := filepath.Join(testDir, "server")
|
||||
//serverLog := filepath.Join(testDir, "gaia-node.log")
|
||||
|
||||
// get RICH
|
||||
keyOut, err := exec.Command(GAIA, CLIENT_EXE, "keys", "get", "alice").Output()
|
||||
if err != nil {
|
||||
fmt.Println("one")
|
||||
return err
|
||||
}
|
||||
key := strings.Split(string(keyOut), "\t")
|
||||
fmt.Printf("wit:%s", key[2])
|
||||
|
||||
outByte, err := exec.Command(GAIA, SERVER_EXE, "init", "--static", fmt.Sprintf("--chain-id=%s", chainID), fmt.Sprintf("--home=%s", serveDir), key[2]).Output()
|
||||
if err != nil {
|
||||
fmt.Println("teo")
|
||||
fmt.Printf("Error: %v", err)
|
||||
|
||||
return err
|
||||
}
|
||||
fmt.Sprintf("OUT: %s", string(outByte))
|
||||
return nil
|
||||
}
|
||||
|
||||
*/
|
|
@ -139,8 +139,14 @@ func (coins Coins) IsGTE(coinsB Coins) bool {
|
|||
}
|
||||
|
||||
// IsZero returns true if there are no coins
|
||||
// or all coins are zero.
|
||||
func (coins Coins) IsZero() bool {
|
||||
return len(coins) == 0
|
||||
for _, coin := range coins {
|
||||
if !coin.IsZero() {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// IsEqual returns true if the two sets of Coins have the same value
|
||||
|
|
|
@ -27,6 +27,7 @@ const (
|
|||
CodeInsufficientFunds CodeType = 5
|
||||
CodeUnknownRequest CodeType = 6
|
||||
CodeUnrecognizedAddress CodeType = 7
|
||||
CodeInvalidPubKey CodeType = 8
|
||||
|
||||
CodeGenesisParse CodeType = 0xdead // TODO: remove ?
|
||||
)
|
||||
|
@ -50,6 +51,8 @@ func CodeToDefaultMsg(code CodeType) string {
|
|||
return "Unknown request"
|
||||
case CodeUnrecognizedAddress:
|
||||
return "Unrecognized address"
|
||||
case CodeInvalidPubKey:
|
||||
return "Invalid pubkey"
|
||||
default:
|
||||
return fmt.Sprintf("Unknown code %d", code)
|
||||
}
|
||||
|
@ -81,8 +84,11 @@ func ErrInsufficientFunds(msg string) Error {
|
|||
func ErrUnknownRequest(msg string) Error {
|
||||
return newError(CodeUnknownRequest, msg)
|
||||
}
|
||||
func ErrUnrecognizedAddress(addr Address) Error {
|
||||
return newError(CodeUnrecognizedAddress, addr.String())
|
||||
func ErrUnrecognizedAddress(msg string) Error {
|
||||
return newError(CodeUnrecognizedAddress, msg)
|
||||
}
|
||||
func ErrInvalidPubKey(msg string) Error {
|
||||
return newError(CodeInvalidPubKey, msg)
|
||||
}
|
||||
|
||||
//----------------------------------------
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
package types
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var codeTypes = []CodeType{
|
||||
CodeInternal,
|
||||
CodeTxParse,
|
||||
CodeInvalidSequence,
|
||||
CodeUnauthorized,
|
||||
CodeInsufficientFunds,
|
||||
CodeUnknownRequest,
|
||||
CodeUnrecognizedAddress,
|
||||
CodeInvalidPubKey,
|
||||
CodeGenesisParse,
|
||||
}
|
||||
|
||||
type errFn func(msg string) Error
|
||||
|
||||
var errFns = []errFn{
|
||||
ErrInternal,
|
||||
ErrTxParse,
|
||||
ErrInvalidSequence,
|
||||
ErrUnauthorized,
|
||||
ErrInsufficientFunds,
|
||||
ErrUnknownRequest,
|
||||
ErrUnrecognizedAddress,
|
||||
ErrInvalidPubKey,
|
||||
ErrGenesisParse,
|
||||
}
|
||||
|
||||
func TestCodeType(t *testing.T) {
|
||||
assert.True(t, CodeOK.IsOK())
|
||||
|
||||
for _, c := range codeTypes {
|
||||
assert.False(t, c.IsOK())
|
||||
msg := CodeToDefaultMsg(c)
|
||||
assert.False(t, strings.HasPrefix(msg, "Unknown code"))
|
||||
}
|
||||
}
|
||||
|
||||
func TestErrFn(t *testing.T) {
|
||||
for i, errFn := range errFns {
|
||||
err := errFn("")
|
||||
codeType := codeTypes[i]
|
||||
assert.Equal(t, err.ABCICode(), codeType)
|
||||
assert.Equal(t, err.Result().Code, codeType)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,232 @@
|
|||
package types
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
wire "github.com/cosmos/cosmos-sdk/wire"
|
||||
)
|
||||
|
||||
type ListMapper interface { // Solidity list like structure
|
||||
Len(sdk.Context) int64
|
||||
Get(sdk.Context, int64, interface{})
|
||||
Set(sdk.Context, int64, interface{})
|
||||
Push(sdk.Context, interface{})
|
||||
Iterate(sdk.Context, interface{}, func(sdk.Context, int64))
|
||||
}
|
||||
|
||||
type listMapper struct {
|
||||
key sdk.StoreKey
|
||||
cdc *wire.Codec
|
||||
lk []byte
|
||||
}
|
||||
|
||||
func NewListMapper(cdc *wire.Codec, key sdk.StoreKey) ListMapper {
|
||||
lk, err := cdc.MarshalBinary(int64(-1))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return listMapper{
|
||||
key: key,
|
||||
cdc: cdc,
|
||||
lk: lk,
|
||||
}
|
||||
}
|
||||
|
||||
func (lm listMapper) Len(ctx sdk.Context) int64 {
|
||||
store := ctx.KVStore(lm.key)
|
||||
bz := store.Get(lm.lk)
|
||||
if bz == nil {
|
||||
zero, err := lm.cdc.MarshalBinary(0)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
store.Set(lm.lk, zero)
|
||||
return 0
|
||||
}
|
||||
var res int64
|
||||
if err := lm.cdc.UnmarshalBinary(bz, &res); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func (lm listMapper) Get(ctx sdk.Context, index int64, ptr interface{}) {
|
||||
if index < 0 {
|
||||
panic(errors.New(""))
|
||||
}
|
||||
store := ctx.KVStore(lm.key)
|
||||
bz := store.Get(marshalInt64(lm.cdc, index))
|
||||
if err := lm.cdc.UnmarshalBinary(bz, ptr); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (lm listMapper) Set(ctx sdk.Context, index int64, value interface{}) {
|
||||
if index < 0 {
|
||||
panic(errors.New(""))
|
||||
}
|
||||
store := ctx.KVStore(lm.key)
|
||||
bz, err := lm.cdc.MarshalBinary(value)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
store.Set(marshalInt64(lm.cdc, index), bz)
|
||||
}
|
||||
|
||||
func (lm listMapper) Push(ctx sdk.Context, value interface{}) {
|
||||
length := lm.Len(ctx)
|
||||
lm.Set(ctx, length, value)
|
||||
|
||||
store := ctx.KVStore(lm.key)
|
||||
store.Set(lm.lk, marshalInt64(lm.cdc, length+1))
|
||||
}
|
||||
|
||||
func (lm listMapper) Iterate(ctx sdk.Context, ptr interface{}, fn func(sdk.Context, int64)) {
|
||||
length := lm.Len(ctx)
|
||||
for i := int64(0); i < length; i++ {
|
||||
lm.Get(ctx, i, ptr)
|
||||
fn(ctx, i)
|
||||
}
|
||||
}
|
||||
|
||||
type QueueMapper interface {
|
||||
Push(sdk.Context, interface{})
|
||||
Peek(sdk.Context, interface{})
|
||||
Pop(sdk.Context)
|
||||
IsEmpty(sdk.Context) bool
|
||||
Iterate(sdk.Context, interface{}, func(sdk.Context) bool)
|
||||
}
|
||||
|
||||
type queueMapper struct {
|
||||
key sdk.StoreKey
|
||||
cdc *wire.Codec
|
||||
ik []byte
|
||||
}
|
||||
|
||||
func NewQueueMapper(cdc *wire.Codec, key sdk.StoreKey) QueueMapper {
|
||||
ik, err := cdc.MarshalBinary(int64(-1))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return queueMapper{
|
||||
key: key,
|
||||
cdc: cdc,
|
||||
ik: ik,
|
||||
}
|
||||
}
|
||||
|
||||
type queueInfo struct {
|
||||
// begin <= elems < end
|
||||
Begin int64
|
||||
End int64
|
||||
}
|
||||
|
||||
func (info queueInfo) validateBasic() error {
|
||||
if info.End < info.Begin || info.Begin < 0 || info.End < 0 {
|
||||
return errors.New("")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (info queueInfo) isEmpty() bool {
|
||||
return info.Begin == info.End
|
||||
}
|
||||
|
||||
func (qm queueMapper) getQueueInfo(store sdk.KVStore) queueInfo {
|
||||
bz := store.Get(qm.ik)
|
||||
if bz == nil {
|
||||
store.Set(qm.ik, marshalQueueInfo(qm.cdc, queueInfo{0, 0}))
|
||||
return queueInfo{0, 0}
|
||||
}
|
||||
var info queueInfo
|
||||
if err := qm.cdc.UnmarshalBinary(bz, &info); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := info.validateBasic(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return info
|
||||
}
|
||||
|
||||
func (qm queueMapper) setQueueInfo(store sdk.KVStore, info queueInfo) {
|
||||
bz, err := qm.cdc.MarshalBinary(info)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
store.Set(qm.ik, bz)
|
||||
}
|
||||
|
||||
func (qm queueMapper) Push(ctx sdk.Context, value interface{}) {
|
||||
store := ctx.KVStore(qm.key)
|
||||
info := qm.getQueueInfo(store)
|
||||
|
||||
bz, err := qm.cdc.MarshalBinary(value)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
store.Set(marshalInt64(qm.cdc, info.End), bz)
|
||||
|
||||
info.End++
|
||||
qm.setQueueInfo(store, info)
|
||||
}
|
||||
|
||||
func (qm queueMapper) Peek(ctx sdk.Context, ptr interface{}) {
|
||||
store := ctx.KVStore(qm.key)
|
||||
info := qm.getQueueInfo(store)
|
||||
bz := store.Get(marshalInt64(qm.cdc, info.Begin))
|
||||
if err := qm.cdc.UnmarshalBinary(bz, ptr); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (qm queueMapper) Pop(ctx sdk.Context) {
|
||||
store := ctx.KVStore(qm.key)
|
||||
info := qm.getQueueInfo(store)
|
||||
store.Delete(marshalInt64(qm.cdc, info.Begin))
|
||||
info.Begin++
|
||||
qm.setQueueInfo(store, info)
|
||||
}
|
||||
|
||||
func (qm queueMapper) IsEmpty(ctx sdk.Context) bool {
|
||||
store := ctx.KVStore(qm.key)
|
||||
info := qm.getQueueInfo(store)
|
||||
return info.isEmpty()
|
||||
}
|
||||
|
||||
func (qm queueMapper) Iterate(ctx sdk.Context, ptr interface{}, fn func(sdk.Context) bool) {
|
||||
store := ctx.KVStore(qm.key)
|
||||
info := qm.getQueueInfo(store)
|
||||
|
||||
var i int64
|
||||
for i = info.Begin; i < info.End; i++ {
|
||||
key := marshalInt64(qm.cdc, i)
|
||||
bz := store.Get(key)
|
||||
if err := qm.cdc.UnmarshalBinary(bz, ptr); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
store.Delete(key)
|
||||
if fn(ctx) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
info.Begin = i
|
||||
qm.setQueueInfo(store, info)
|
||||
}
|
||||
|
||||
func marshalQueueInfo(cdc *wire.Codec, info queueInfo) []byte {
|
||||
bz, err := cdc.MarshalBinary(info)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return bz
|
||||
}
|
||||
|
||||
func marshalInt64(cdc *wire.Codec, i int64) []byte {
|
||||
bz, err := cdc.MarshalBinary(i)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return bz
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
package types
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
dbm "github.com/tendermint/tmlibs/db"
|
||||
|
||||
abci "github.com/tendermint/abci/types"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/store"
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
wire "github.com/cosmos/cosmos-sdk/wire"
|
||||
)
|
||||
|
||||
type S struct {
|
||||
I int64
|
||||
B bool
|
||||
}
|
||||
|
||||
func defaultComponents(key sdk.StoreKey) (sdk.Context, *wire.Codec) {
|
||||
db := dbm.NewMemDB()
|
||||
cms := store.NewCommitMultiStore(db)
|
||||
cms.MountStoreWithDB(key, sdk.StoreTypeIAVL, db)
|
||||
cms.LoadLatestVersion()
|
||||
ctx := sdk.NewContext(cms, abci.Header{}, false, nil)
|
||||
cdc := wire.NewCodec()
|
||||
return ctx, cdc
|
||||
}
|
||||
|
||||
func TestListMapper(t *testing.T) {
|
||||
key := sdk.NewKVStoreKey("list")
|
||||
ctx, cdc := defaultComponents(key)
|
||||
lm := NewListMapper(cdc, key)
|
||||
|
||||
val := S{1, true}
|
||||
var res S
|
||||
|
||||
lm.Push(ctx, val)
|
||||
assert.Equal(t, int64(1), lm.Len(ctx))
|
||||
lm.Get(ctx, int64(0), &res)
|
||||
assert.Equal(t, val, res)
|
||||
|
||||
val = S{2, false}
|
||||
lm.Set(ctx, int64(0), val)
|
||||
lm.Get(ctx, int64(0), &res)
|
||||
assert.Equal(t, val, res)
|
||||
}
|
||||
|
||||
func TestQueueMapper(t *testing.T) {
|
||||
key := sdk.NewKVStoreKey("queue")
|
||||
ctx, cdc := defaultComponents(key)
|
||||
qm := NewQueueMapper(cdc, key)
|
||||
|
||||
val := S{1, true}
|
||||
var res S
|
||||
|
||||
qm.Push(ctx, val)
|
||||
qm.Peek(ctx, &res)
|
||||
assert.Equal(t, val, res)
|
||||
|
||||
qm.Pop(ctx)
|
||||
empty := qm.IsEmpty(ctx)
|
||||
|
||||
assert.Equal(t, true, empty)
|
||||
|
||||
assert.Panics(t, func() { qm.Peek(ctx, &res) })
|
||||
}
|
110
types/tx_msg.go
110
types/tx_msg.go
|
@ -1,6 +1,8 @@
|
|||
package types
|
||||
|
||||
import "encoding/json"
|
||||
import (
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
// Transactions messages must fulfill the Msg
|
||||
type Msg interface {
|
||||
|
@ -33,10 +35,6 @@ type Tx interface {
|
|||
// Gets the Msg.
|
||||
GetMsg() Msg
|
||||
|
||||
// The address that pays the base fee for this message. The fee is
|
||||
// deducted before the Msg is processed.
|
||||
GetFeePayer() Address
|
||||
|
||||
// Signatures returns the signature of signers who signed the Msg.
|
||||
// CONTRACT: Length returned is same as length of
|
||||
// pubkeys returned from MsgKeySigners, and the order
|
||||
|
@ -49,25 +47,67 @@ type Tx interface {
|
|||
|
||||
var _ Tx = (*StdTx)(nil)
|
||||
|
||||
// StdTx is a standard way to wrap a Msg with Signatures.
|
||||
// StdTx is a standard way to wrap a Msg with Fee and Signatures.
|
||||
// NOTE: the first signature is the FeePayer (Signatures must not be nil).
|
||||
type StdTx struct {
|
||||
Msg `json:"msg"`
|
||||
Fee StdFee `json:"fee"`
|
||||
Signatures []StdSignature `json:"signatures"`
|
||||
}
|
||||
|
||||
func NewStdTx(msg Msg, sigs []StdSignature) StdTx {
|
||||
func NewStdTx(msg Msg, fee StdFee, sigs []StdSignature) StdTx {
|
||||
return StdTx{
|
||||
Msg: msg,
|
||||
Fee: fee,
|
||||
Signatures: sigs,
|
||||
}
|
||||
}
|
||||
|
||||
//nolint
|
||||
func (tx StdTx) GetMsg() Msg { return tx.Msg }
|
||||
func (tx StdTx) GetFeePayer() Address { return tx.Signatures[0].PubKey.Address() } // XXX but PubKey is optional!
|
||||
func (tx StdTx) GetSignatures() []StdSignature { return tx.Signatures }
|
||||
|
||||
// FeePayer returns the address responsible for paying the fees
|
||||
// for the transactions. It's the first address returned by msg.GetSigners().
|
||||
// If GetSigners() is empty, this panics.
|
||||
func FeePayer(tx Tx) Address {
|
||||
return tx.GetMsg().GetSigners()[0]
|
||||
}
|
||||
|
||||
//__________________________________________________________
|
||||
|
||||
// StdFee includes the amount of coins paid in fees and the maximum
|
||||
// gas to be used by the transaction. The ratio yields an effective "gasprice",
|
||||
// which must be above some miminum to be accepted into the mempool.
|
||||
type StdFee struct {
|
||||
Amount Coins `json"amount"`
|
||||
Gas int64 `json"gas"`
|
||||
}
|
||||
|
||||
func NewStdFee(gas int64, amount ...Coin) StdFee {
|
||||
return StdFee{
|
||||
Amount: amount,
|
||||
Gas: gas,
|
||||
}
|
||||
}
|
||||
|
||||
func (fee StdFee) Bytes() []byte {
|
||||
// normalize. XXX
|
||||
// this is a sign of something ugly
|
||||
// (in the lcd_test, client side its null,
|
||||
// server side its [])
|
||||
if len(fee.Amount) == 0 {
|
||||
fee.Amount = Coins{}
|
||||
}
|
||||
bz, err := json.Marshal(fee) // TODO
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return bz
|
||||
}
|
||||
|
||||
//__________________________________________________________
|
||||
|
||||
// StdSignDoc is replay-prevention structure.
|
||||
// It includes the result of msg.GetSignBytes(),
|
||||
// as well as the ChainID (prevent cross chain replay)
|
||||
|
@ -76,14 +116,18 @@ func (tx StdTx) GetSignatures() []StdSignature { return tx.Signatures }
|
|||
type StdSignDoc struct {
|
||||
ChainID string `json:"chain_id"`
|
||||
Sequences []int64 `json:"sequences"`
|
||||
FeeBytes []byte `json:"fee_bytes"`
|
||||
MsgBytes []byte `json:"msg_bytes"`
|
||||
AltBytes []byte `json:"alt_bytes"` // TODO: do we really want this ?
|
||||
AltBytes []byte `json:"alt_bytes"`
|
||||
}
|
||||
|
||||
func StdSignBytes(chainID string, sequences []int64, msg Msg) []byte {
|
||||
// StdSignBytes returns the bytes to sign for a transaction.
|
||||
// TODO: change the API to just take a chainID and StdTx ?
|
||||
func StdSignBytes(chainID string, sequences []int64, fee StdFee, msg Msg) []byte {
|
||||
bz, err := json.Marshal(StdSignDoc{
|
||||
ChainID: chainID,
|
||||
Sequences: sequences,
|
||||
FeeBytes: fee.Bytes(),
|
||||
MsgBytes: msg.GetSignBytes(),
|
||||
})
|
||||
if err != nil {
|
||||
|
@ -92,7 +136,51 @@ func StdSignBytes(chainID string, sequences []int64, msg Msg) []byte {
|
|||
return bz
|
||||
}
|
||||
|
||||
//-------------------------------------
|
||||
// StdSignMsg is a convenience structure for passing along
|
||||
// a Msg with the other requirements for a StdSignDoc before
|
||||
// it is signed. For use in the CLI.
|
||||
type StdSignMsg struct {
|
||||
ChainID string
|
||||
Sequences []int64
|
||||
Fee StdFee
|
||||
Msg Msg
|
||||
// XXX: Alt
|
||||
}
|
||||
|
||||
func (msg StdSignMsg) Bytes() []byte {
|
||||
return StdSignBytes(msg.ChainID, msg.Sequences, msg.Fee, msg.Msg)
|
||||
}
|
||||
|
||||
//__________________________________________________________
|
||||
|
||||
// Application function variable used to unmarshal transaction bytes
|
||||
type TxDecoder func(txBytes []byte) (Tx, Error)
|
||||
|
||||
//__________________________________________________________
|
||||
|
||||
var _ Msg = (*TestMsg)(nil)
|
||||
|
||||
// msg type for testing
|
||||
type TestMsg struct {
|
||||
signers []Address
|
||||
}
|
||||
|
||||
func NewTestMsg(addrs ...Address) *TestMsg {
|
||||
return &TestMsg{
|
||||
signers: addrs,
|
||||
}
|
||||
}
|
||||
|
||||
func (msg *TestMsg) Type() string { return "TestMsg" }
|
||||
func (msg *TestMsg) Get(key interface{}) (value interface{}) { return nil }
|
||||
func (msg *TestMsg) GetSignBytes() []byte {
|
||||
bz, err := json.Marshal(msg.signers)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return bz
|
||||
}
|
||||
func (msg *TestMsg) ValidateBasic() Error { return nil }
|
||||
func (msg *TestMsg) GetSigners() []Address {
|
||||
return msg.signers
|
||||
}
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
package types
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
crypto "github.com/tendermint/go-crypto"
|
||||
)
|
||||
|
||||
func newStdFee() StdFee {
|
||||
return NewStdFee(100,
|
||||
Coin{"atom", 150},
|
||||
)
|
||||
}
|
||||
|
||||
func TestStdTx(t *testing.T) {
|
||||
priv := crypto.GenPrivKeyEd25519()
|
||||
addr := priv.PubKey().Address()
|
||||
msg := NewTestMsg(addr)
|
||||
fee := newStdFee()
|
||||
sigs := []StdSignature{}
|
||||
|
||||
tx := NewStdTx(msg, fee, sigs)
|
||||
assert.Equal(t, msg, tx.GetMsg())
|
||||
assert.Equal(t, sigs, tx.GetSignatures())
|
||||
|
||||
feePayer := FeePayer(tx)
|
||||
assert.Equal(t, addr, feePayer)
|
||||
}
|
|
@ -2,6 +2,7 @@ package version
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
@ -11,14 +12,28 @@ var (
|
|||
VersionCmd = &cobra.Command{
|
||||
Use: "version",
|
||||
Short: "Print the app version",
|
||||
Run: doVersionCmd,
|
||||
Run: printVersion,
|
||||
}
|
||||
)
|
||||
|
||||
func doVersionCmd(cmd *cobra.Command, args []string) {
|
||||
func getVersion() string {
|
||||
v := Version
|
||||
if GitCommit != "" {
|
||||
v = v + " " + GitCommit
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// CMD
|
||||
|
||||
func printVersion(cmd *cobra.Command, args []string) {
|
||||
v := getVersion()
|
||||
fmt.Println(v)
|
||||
}
|
||||
|
||||
// REST
|
||||
|
||||
func VersionRequestHandler(w http.ResponseWriter, r *http.Request) {
|
||||
v := getVersion()
|
||||
w.Write([]byte(v))
|
||||
}
|
||||
|
|
110
x/auth/ante.go
110
x/auth/ante.go
|
@ -1,11 +1,16 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
// NewAnteHandler returns an AnteHandler that checks
|
||||
// and increments sequence numbers, checks signatures,
|
||||
// and deducts fees from the first signer.
|
||||
func NewAnteHandler(accountMapper sdk.AccountMapper) sdk.AnteHandler {
|
||||
return func(
|
||||
ctx sdk.Context, tx sdk.Tx,
|
||||
|
@ -22,6 +27,12 @@ func NewAnteHandler(accountMapper sdk.AccountMapper) sdk.AnteHandler {
|
|||
// TODO: can tx just implement message?
|
||||
msg := tx.GetMsg()
|
||||
|
||||
// TODO: will this always be a stdtx? should that be used in the function signature?
|
||||
stdTx, ok := tx.(sdk.StdTx)
|
||||
if !ok {
|
||||
return ctx, sdk.ErrInternal("tx must be sdk.StdTx").Result(), true
|
||||
}
|
||||
|
||||
// Assert that number of signatures is correct.
|
||||
var signerAddrs = msg.GetSigners()
|
||||
if len(sigs) != len(signerAddrs) {
|
||||
|
@ -30,51 +41,70 @@ func NewAnteHandler(accountMapper sdk.AccountMapper) sdk.AnteHandler {
|
|||
true
|
||||
}
|
||||
|
||||
// Collect accounts to set in the context
|
||||
var signerAccs = make([]sdk.Account, len(signerAddrs))
|
||||
|
||||
// Get the sign bytes by collecting all sequence numbers
|
||||
// Get the sign bytes (requires all sequence numbers and the fee)
|
||||
sequences := make([]int64, len(signerAddrs))
|
||||
for i := 0; i < len(signerAddrs); i++ {
|
||||
sequences[i] = sigs[i].Sequence
|
||||
}
|
||||
signBytes := sdk.StdSignBytes(ctx.ChainID(), sequences, msg)
|
||||
|
||||
// Check fee payer sig and nonce, and deduct fee.
|
||||
// This is done first because it only
|
||||
// requires fetching 1 account.
|
||||
payerAddr, payerSig := signerAddrs[0], sigs[0]
|
||||
payerAcc, res := processSig(ctx, accountMapper, payerAddr, payerSig, signBytes)
|
||||
if !res.IsOK() {
|
||||
return ctx, res, true
|
||||
fee := stdTx.Fee
|
||||
chainID := ctx.ChainID()
|
||||
// XXX: major hack; need to get ChainID
|
||||
// into the app right away (#565)
|
||||
if chainID == "" {
|
||||
chainID = viper.GetString("chain-id")
|
||||
}
|
||||
signerAccs[0] = payerAcc
|
||||
// TODO: Charge fee from payerAcc.
|
||||
// TODO: accountMapper.SetAccount(ctx, payerAddr)
|
||||
signBytes := sdk.StdSignBytes(ctx.ChainID(), sequences, fee, msg)
|
||||
|
||||
// Check sig and nonce for the rest.
|
||||
for i := 1; i < len(sigs); i++ {
|
||||
// Check sig and nonce and collect signer accounts.
|
||||
var signerAccs = make([]sdk.Account, len(signerAddrs))
|
||||
for i := 0; i < len(sigs); i++ {
|
||||
signerAddr, sig := signerAddrs[i], sigs[i]
|
||||
signerAcc, res := processSig(ctx, accountMapper, signerAddr, sig, signBytes)
|
||||
|
||||
// check signature, return account with incremented nonce
|
||||
signerAcc, res := processSig(
|
||||
ctx, accountMapper,
|
||||
signerAddr, sig, signBytes,
|
||||
)
|
||||
if !res.IsOK() {
|
||||
return ctx, res, true
|
||||
}
|
||||
|
||||
// first sig pays the fees
|
||||
if i == 0 {
|
||||
// TODO: min fee
|
||||
if !fee.Amount.IsZero() {
|
||||
signerAcc, res = deductFees(signerAcc, fee)
|
||||
if !res.IsOK() {
|
||||
return ctx, res, true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Save the account.
|
||||
accountMapper.SetAccount(ctx, signerAcc)
|
||||
signerAccs[i] = signerAcc
|
||||
}
|
||||
|
||||
// cache the signer accounts in the context
|
||||
ctx = WithSigners(ctx, signerAccs)
|
||||
|
||||
// TODO: tx tags (?)
|
||||
|
||||
return ctx, sdk.Result{}, false // continue...
|
||||
}
|
||||
}
|
||||
|
||||
// verify the signature and increment the sequence.
|
||||
// if the account doesn't have a pubkey, set it as well.
|
||||
func processSig(ctx sdk.Context, am sdk.AccountMapper, addr sdk.Address, sig sdk.StdSignature, signBytes []byte) (acc sdk.Account, res sdk.Result) {
|
||||
// if the account doesn't have a pubkey, set it.
|
||||
func processSig(
|
||||
ctx sdk.Context, am sdk.AccountMapper,
|
||||
addr sdk.Address, sig sdk.StdSignature, signBytes []byte) (
|
||||
acc sdk.Account, res sdk.Result) {
|
||||
|
||||
// Get the account
|
||||
// Get the account.
|
||||
acc = am.GetAccount(ctx, addr)
|
||||
if acc == nil {
|
||||
return nil, sdk.ErrUnrecognizedAddress(addr).Result()
|
||||
return nil, sdk.ErrUnrecognizedAddress(addr.String()).Result()
|
||||
}
|
||||
|
||||
// Check and increment sequence number.
|
||||
|
@ -85,24 +115,44 @@ func processSig(ctx sdk.Context, am sdk.AccountMapper, addr sdk.Address, sig sdk
|
|||
}
|
||||
acc.SetSequence(seq + 1)
|
||||
|
||||
// Check and possibly set pubkey.
|
||||
// If pubkey is not known for account,
|
||||
// set it from the StdSignature.
|
||||
pubKey := acc.GetPubKey()
|
||||
if pubKey.Empty() {
|
||||
pubKey = sig.PubKey
|
||||
if pubKey.Empty() {
|
||||
return nil, sdk.ErrInvalidPubKey("PubKey not found").Result()
|
||||
}
|
||||
if !bytes.Equal(pubKey.Address(), addr) {
|
||||
return nil, sdk.ErrInvalidPubKey(
|
||||
fmt.Sprintf("PubKey does not match Signer address %v", addr)).Result()
|
||||
}
|
||||
err := acc.SetPubKey(pubKey)
|
||||
if err != nil {
|
||||
return nil, sdk.ErrInternal("setting PubKey on signer").Result()
|
||||
return nil, sdk.ErrInternal("setting PubKey on signer's account").Result()
|
||||
}
|
||||
}
|
||||
// TODO: should we enforce pubKey == sig.PubKey ?
|
||||
// If not, ppl can send useless PubKeys after first tx
|
||||
|
||||
// Check sig.
|
||||
if !sig.PubKey.VerifyBytes(signBytes, sig.Signature) {
|
||||
if !pubKey.VerifyBytes(signBytes, sig.Signature) {
|
||||
return nil, sdk.ErrUnauthorized("signature verification failed").Result()
|
||||
}
|
||||
|
||||
// Save the account.
|
||||
am.SetAccount(ctx, acc)
|
||||
return
|
||||
}
|
||||
|
||||
// Deduct the fee from the account.
|
||||
// We could use the CoinKeeper (in addition to the AccountMapper,
|
||||
// because the CoinKeeper doesn't give us accounts), but it seems easier to do this.
|
||||
func deductFees(acc sdk.Account, fee sdk.StdFee) (sdk.Account, sdk.Result) {
|
||||
coins := acc.GetCoins()
|
||||
feeAmount := fee.Amount
|
||||
|
||||
newCoins := coins.Minus(feeAmount)
|
||||
if !newCoins.IsNotNegative() {
|
||||
errMsg := fmt.Sprintf("%s < %s", coins, feeAmount)
|
||||
return nil, sdk.ErrInsufficientFunds(errMsg).Result()
|
||||
}
|
||||
acc.SetCoins(newCoins)
|
||||
return acc, sdk.Result{}
|
||||
}
|
||||
|
|
|
@ -5,33 +5,28 @@ import (
|
|||
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
abci "github.com/tendermint/abci/types"
|
||||
crypto "github.com/tendermint/go-crypto"
|
||||
)
|
||||
|
||||
// msg type for testing
|
||||
type testMsg struct {
|
||||
signBytes []byte
|
||||
signers []sdk.Address
|
||||
func newTestMsg(addrs ...sdk.Address) *sdk.TestMsg {
|
||||
return sdk.NewTestMsg(addrs...)
|
||||
}
|
||||
|
||||
func newTestMsg(addrs ...sdk.Address) *testMsg {
|
||||
return &testMsg{
|
||||
signBytes: []byte("some sign bytes"),
|
||||
signers: addrs,
|
||||
func newStdFee() sdk.StdFee {
|
||||
return sdk.NewStdFee(100,
|
||||
sdk.Coin{"atom", 150},
|
||||
)
|
||||
}
|
||||
|
||||
// coins to more than cover the fee
|
||||
func newCoins() sdk.Coins {
|
||||
return sdk.Coins{
|
||||
{"atom", 10000000},
|
||||
}
|
||||
}
|
||||
|
||||
func (msg *testMsg) Type() string { return "testMsg" }
|
||||
func (msg *testMsg) Get(key interface{}) (value interface{}) { return nil }
|
||||
func (msg *testMsg) GetSignBytes() []byte {
|
||||
return msg.signBytes
|
||||
}
|
||||
func (msg *testMsg) ValidateBasic() sdk.Error { return nil }
|
||||
func (msg *testMsg) GetSigners() []sdk.Address {
|
||||
return msg.signers
|
||||
}
|
||||
|
||||
// generate a priv key and return it with its address
|
||||
func privAndAddr() (crypto.PrivKey, sdk.Address) {
|
||||
priv := crypto.GenPrivKeyEd25519()
|
||||
|
@ -54,13 +49,18 @@ func checkInvalidTx(t *testing.T, anteHandler sdk.AnteHandler, ctx sdk.Context,
|
|||
assert.Equal(t, code, result.Code)
|
||||
}
|
||||
|
||||
func newTestTx(ctx sdk.Context, msg sdk.Msg, privs []crypto.PrivKey, seqs []int64) sdk.Tx {
|
||||
signBytes := sdk.StdSignBytes(ctx.ChainID(), seqs, msg)
|
||||
func newTestTx(ctx sdk.Context, msg sdk.Msg, privs []crypto.PrivKey, seqs []int64, fee sdk.StdFee) sdk.Tx {
|
||||
signBytes := sdk.StdSignBytes(ctx.ChainID(), seqs, fee, msg)
|
||||
return newTestTxWithSignBytes(msg, privs, seqs, fee, signBytes)
|
||||
}
|
||||
|
||||
func newTestTxWithSignBytes(msg sdk.Msg, privs []crypto.PrivKey, seqs []int64, fee sdk.StdFee, signBytes []byte) sdk.Tx {
|
||||
sigs := make([]sdk.StdSignature, len(privs))
|
||||
for i, priv := range privs {
|
||||
sigs[i] = sdk.StdSignature{PubKey: priv.PubKey(), Signature: priv.Sign(signBytes), Sequence: seqs[i]}
|
||||
}
|
||||
return sdk.NewStdTx(msg, sigs)
|
||||
tx := sdk.NewStdTx(msg, fee, sigs)
|
||||
return tx
|
||||
}
|
||||
|
||||
// Test various error cases in the AnteHandler control flow.
|
||||
|
@ -78,22 +78,26 @@ func TestAnteHandlerSigErrors(t *testing.T) {
|
|||
// msg and signatures
|
||||
var tx sdk.Tx
|
||||
msg := newTestMsg(addr1, addr2)
|
||||
tx = newTestTx(ctx, msg, []crypto.PrivKey{priv1, priv2}, []int64{0, 0})
|
||||
fee := newStdFee()
|
||||
|
||||
// test no signatures
|
||||
tx = newTestTx(ctx, msg, []crypto.PrivKey{}, []int64{})
|
||||
privs, seqs := []crypto.PrivKey{}, []int64{}
|
||||
tx = newTestTx(ctx, msg, privs, seqs, fee)
|
||||
checkInvalidTx(t, anteHandler, ctx, tx, sdk.CodeUnauthorized)
|
||||
|
||||
// test num sigs dont match GetSigners
|
||||
tx = newTestTx(ctx, msg, []crypto.PrivKey{priv1}, []int64{0})
|
||||
privs, seqs = []crypto.PrivKey{priv1}, []int64{0}
|
||||
tx = newTestTx(ctx, msg, privs, seqs, fee)
|
||||
checkInvalidTx(t, anteHandler, ctx, tx, sdk.CodeUnauthorized)
|
||||
|
||||
// test an unrecognized account
|
||||
tx = newTestTx(ctx, msg, []crypto.PrivKey{priv1, priv2}, []int64{0, 0})
|
||||
privs, seqs = []crypto.PrivKey{priv1, priv2}, []int64{0, 0}
|
||||
tx = newTestTx(ctx, msg, privs, seqs, fee)
|
||||
checkInvalidTx(t, anteHandler, ctx, tx, sdk.CodeUnrecognizedAddress)
|
||||
|
||||
// save the first account, but second is still unrecognized
|
||||
acc1 := mapper.NewAccountWithAddress(ctx, addr1)
|
||||
acc1.SetCoins(fee.Amount)
|
||||
mapper.SetAccount(ctx, acc1)
|
||||
checkInvalidTx(t, anteHandler, ctx, tx, sdk.CodeUnrecognizedAddress)
|
||||
}
|
||||
|
@ -112,28 +116,34 @@ func TestAnteHandlerSequences(t *testing.T) {
|
|||
|
||||
// set the accounts
|
||||
acc1 := mapper.NewAccountWithAddress(ctx, addr1)
|
||||
acc1.SetCoins(newCoins())
|
||||
mapper.SetAccount(ctx, acc1)
|
||||
acc2 := mapper.NewAccountWithAddress(ctx, addr2)
|
||||
acc2.SetCoins(newCoins())
|
||||
mapper.SetAccount(ctx, acc2)
|
||||
|
||||
// msg and signatures
|
||||
var tx sdk.Tx
|
||||
msg := newTestMsg(addr1)
|
||||
tx = newTestTx(ctx, msg, []crypto.PrivKey{priv1}, []int64{0})
|
||||
fee := newStdFee()
|
||||
|
||||
// test good tx from one signer
|
||||
privs, seqs := []crypto.PrivKey{priv1}, []int64{0}
|
||||
tx = newTestTx(ctx, msg, privs, seqs, fee)
|
||||
checkValidTx(t, anteHandler, ctx, tx)
|
||||
|
||||
// test sending it again fails (replay protection)
|
||||
checkInvalidTx(t, anteHandler, ctx, tx, sdk.CodeInvalidSequence)
|
||||
|
||||
// fix sequence, should pass
|
||||
tx = newTestTx(ctx, msg, []crypto.PrivKey{priv1}, []int64{1})
|
||||
seqs = []int64{1}
|
||||
tx = newTestTx(ctx, msg, privs, seqs, fee)
|
||||
checkValidTx(t, anteHandler, ctx, tx)
|
||||
|
||||
// new tx with another signer and correct sequences
|
||||
msg = newTestMsg(addr1, addr2)
|
||||
tx = newTestTx(ctx, msg, []crypto.PrivKey{priv1, priv2}, []int64{2, 0})
|
||||
privs, seqs = []crypto.PrivKey{priv1, priv2}, []int64{2, 0}
|
||||
tx = newTestTx(ctx, msg, privs, seqs, fee)
|
||||
checkValidTx(t, anteHandler, ctx, tx)
|
||||
|
||||
// replay fails
|
||||
|
@ -141,23 +151,175 @@ func TestAnteHandlerSequences(t *testing.T) {
|
|||
|
||||
// tx from just second signer with incorrect sequence fails
|
||||
msg = newTestMsg(addr2)
|
||||
tx = newTestTx(ctx, msg, []crypto.PrivKey{priv2}, []int64{0})
|
||||
privs, seqs = []crypto.PrivKey{priv2}, []int64{0}
|
||||
tx = newTestTx(ctx, msg, privs, seqs, fee)
|
||||
checkInvalidTx(t, anteHandler, ctx, tx, sdk.CodeInvalidSequence)
|
||||
|
||||
// fix the sequence and it passes
|
||||
tx = newTestTx(ctx, msg, []crypto.PrivKey{priv2}, []int64{1})
|
||||
tx = newTestTx(ctx, msg, []crypto.PrivKey{priv2}, []int64{1}, fee)
|
||||
checkValidTx(t, anteHandler, ctx, tx)
|
||||
|
||||
// another tx from both of them that passes
|
||||
msg = newTestMsg(addr1, addr2)
|
||||
tx = newTestTx(ctx, msg, []crypto.PrivKey{priv1, priv2}, []int64{3, 2})
|
||||
privs, seqs = []crypto.PrivKey{priv1, priv2}, []int64{3, 2}
|
||||
tx = newTestTx(ctx, msg, privs, seqs, fee)
|
||||
checkValidTx(t, anteHandler, ctx, tx)
|
||||
}
|
||||
|
||||
// Test logic around fee deduction.
|
||||
func TestAnteHandlerFees(t *testing.T) {
|
||||
// setup
|
||||
ms, capKey := setupMultiStore()
|
||||
mapper := NewAccountMapper(capKey, &BaseAccount{})
|
||||
anteHandler := NewAnteHandler(mapper)
|
||||
ctx := sdk.NewContext(ms, abci.Header{ChainID: "mychainid"}, false, nil)
|
||||
|
||||
// keys and addresses
|
||||
priv1, addr1 := privAndAddr()
|
||||
|
||||
// set the accounts
|
||||
acc1 := mapper.NewAccountWithAddress(ctx, addr1)
|
||||
mapper.SetAccount(ctx, acc1)
|
||||
|
||||
// msg and signatures
|
||||
var tx sdk.Tx
|
||||
msg := newTestMsg(addr1)
|
||||
privs, seqs := []crypto.PrivKey{priv1}, []int64{0}
|
||||
fee := sdk.NewStdFee(100,
|
||||
sdk.Coin{"atom", 150},
|
||||
)
|
||||
|
||||
// signer does not have enough funds to pay the fee
|
||||
tx = newTestTx(ctx, msg, privs, seqs, fee)
|
||||
checkInvalidTx(t, anteHandler, ctx, tx, sdk.CodeInsufficientFunds)
|
||||
|
||||
acc1.SetCoins(sdk.Coins{{"atom", 149}})
|
||||
mapper.SetAccount(ctx, acc1)
|
||||
checkInvalidTx(t, anteHandler, ctx, tx, sdk.CodeInsufficientFunds)
|
||||
|
||||
acc1.SetCoins(sdk.Coins{{"atom", 150}})
|
||||
mapper.SetAccount(ctx, acc1)
|
||||
checkValidTx(t, anteHandler, ctx, tx)
|
||||
}
|
||||
|
||||
func TestAnteHandlerBadSignBytes(t *testing.T) {
|
||||
// TODO: test various cases of bad sign bytes
|
||||
// setup
|
||||
ms, capKey := setupMultiStore()
|
||||
mapper := NewAccountMapper(capKey, &BaseAccount{})
|
||||
anteHandler := NewAnteHandler(mapper)
|
||||
ctx := sdk.NewContext(ms, abci.Header{ChainID: "mychainid"}, false, nil)
|
||||
|
||||
// keys and addresses
|
||||
priv1, addr1 := privAndAddr()
|
||||
priv2, addr2 := privAndAddr()
|
||||
|
||||
// set the accounts
|
||||
acc1 := mapper.NewAccountWithAddress(ctx, addr1)
|
||||
acc1.SetCoins(newCoins())
|
||||
mapper.SetAccount(ctx, acc1)
|
||||
acc2 := mapper.NewAccountWithAddress(ctx, addr2)
|
||||
acc2.SetCoins(newCoins())
|
||||
mapper.SetAccount(ctx, acc2)
|
||||
|
||||
var tx sdk.Tx
|
||||
msg := newTestMsg(addr1)
|
||||
fee := newStdFee()
|
||||
fee2 := newStdFee()
|
||||
fee2.Gas += 100
|
||||
fee3 := newStdFee()
|
||||
fee3.Amount[0].Amount += 100
|
||||
|
||||
// test good tx and signBytes
|
||||
privs, seqs := []crypto.PrivKey{priv1}, []int64{0}
|
||||
tx = newTestTx(ctx, msg, privs, seqs, fee)
|
||||
checkValidTx(t, anteHandler, ctx, tx)
|
||||
|
||||
chainID := ctx.ChainID()
|
||||
chainID2 := chainID + "somemorestuff"
|
||||
codeUnauth := sdk.CodeUnauthorized
|
||||
|
||||
cases := []struct {
|
||||
chainID string
|
||||
seqs []int64
|
||||
fee sdk.StdFee
|
||||
msg sdk.Msg
|
||||
code sdk.CodeType
|
||||
}{
|
||||
{chainID2, []int64{1}, fee, msg, codeUnauth}, // test wrong chain_id
|
||||
{chainID, []int64{2}, fee, msg, codeUnauth}, // test wrong seqs
|
||||
{chainID, []int64{1, 2}, fee, msg, codeUnauth}, // test wrong seqs
|
||||
{chainID, []int64{1}, fee, newTestMsg(addr2), codeUnauth}, // test wrong msg
|
||||
{chainID, []int64{1}, fee2, newTestMsg(addr2), codeUnauth}, // test wrong fee
|
||||
{chainID, []int64{1}, fee3, newTestMsg(addr2), codeUnauth}, // test wrong fee
|
||||
}
|
||||
|
||||
privs, seqs = []crypto.PrivKey{priv1}, []int64{1}
|
||||
for _, cs := range cases {
|
||||
tx := newTestTxWithSignBytes(
|
||||
msg, privs, seqs, fee,
|
||||
sdk.StdSignBytes(cs.chainID, cs.seqs, cs.fee, cs.msg),
|
||||
)
|
||||
checkInvalidTx(t, anteHandler, ctx, tx, cs.code)
|
||||
}
|
||||
|
||||
// test wrong signer if public key exist
|
||||
privs, seqs = []crypto.PrivKey{priv2}, []int64{1}
|
||||
tx = newTestTx(ctx, msg, privs, seqs, fee)
|
||||
checkInvalidTx(t, anteHandler, ctx, tx, sdk.CodeUnauthorized)
|
||||
|
||||
// test wrong signer if public doesn't exist
|
||||
msg = newTestMsg(addr2)
|
||||
privs, seqs = []crypto.PrivKey{priv1}, []int64{0}
|
||||
tx = newTestTx(ctx, msg, privs, seqs, fee)
|
||||
checkInvalidTx(t, anteHandler, ctx, tx, sdk.CodeInvalidPubKey)
|
||||
|
||||
}
|
||||
|
||||
func TestAnteHandlerSetPubKey(t *testing.T) {
|
||||
// TODO: test cases where pubkey is already set on the account
|
||||
// setup
|
||||
ms, capKey := setupMultiStore()
|
||||
mapper := NewAccountMapper(capKey, &BaseAccount{})
|
||||
anteHandler := NewAnteHandler(mapper)
|
||||
ctx := sdk.NewContext(ms, abci.Header{ChainID: "mychainid"}, false, nil)
|
||||
|
||||
// keys and addresses
|
||||
priv1, addr1 := privAndAddr()
|
||||
_, addr2 := privAndAddr()
|
||||
|
||||
// set the accounts
|
||||
acc1 := mapper.NewAccountWithAddress(ctx, addr1)
|
||||
acc1.SetCoins(newCoins())
|
||||
mapper.SetAccount(ctx, acc1)
|
||||
acc2 := mapper.NewAccountWithAddress(ctx, addr2)
|
||||
acc2.SetCoins(newCoins())
|
||||
mapper.SetAccount(ctx, acc2)
|
||||
|
||||
var tx sdk.Tx
|
||||
|
||||
// test good tx and set public key
|
||||
msg := newTestMsg(addr1)
|
||||
privs, seqs := []crypto.PrivKey{priv1}, []int64{0}
|
||||
fee := newStdFee()
|
||||
tx = newTestTx(ctx, msg, privs, seqs, fee)
|
||||
checkValidTx(t, anteHandler, ctx, tx)
|
||||
|
||||
acc1 = mapper.GetAccount(ctx, addr1)
|
||||
require.Equal(t, acc1.GetPubKey(), priv1.PubKey())
|
||||
|
||||
// test public key not found
|
||||
msg = newTestMsg(addr2)
|
||||
tx = newTestTx(ctx, msg, privs, seqs, fee)
|
||||
sigs := tx.GetSignatures()
|
||||
sigs[0].PubKey = crypto.PubKey{}
|
||||
checkInvalidTx(t, anteHandler, ctx, tx, sdk.CodeInvalidPubKey)
|
||||
|
||||
acc2 = mapper.GetAccount(ctx, addr2)
|
||||
assert.True(t, acc2.GetPubKey().Empty())
|
||||
|
||||
// test invalid signature and public key
|
||||
tx = newTestTx(ctx, msg, privs, seqs, fee)
|
||||
checkInvalidTx(t, anteHandler, ctx, tx, sdk.CodeInvalidPubKey)
|
||||
|
||||
acc2 = mapper.GetAccount(ctx, addr2)
|
||||
assert.True(t, acc2.GetPubKey().Empty())
|
||||
}
|
||||
|
|
|
@ -11,42 +11,107 @@ import (
|
|||
wire "github.com/cosmos/cosmos-sdk/wire"
|
||||
)
|
||||
|
||||
func TestBaseAccount(t *testing.T) {
|
||||
func keyPubAddr() (crypto.PrivKey, crypto.PubKey, sdk.Address) {
|
||||
key := crypto.GenPrivKeyEd25519()
|
||||
pub := key.PubKey()
|
||||
addr := pub.Address()
|
||||
return key.Wrap(), pub, addr
|
||||
}
|
||||
|
||||
func TestBaseAccountAddressPubKey(t *testing.T) {
|
||||
_, pub1, addr1 := keyPubAddr()
|
||||
_, pub2, addr2 := keyPubAddr()
|
||||
acc := NewBaseAccountWithAddress(addr1)
|
||||
|
||||
// check the address (set) and pubkey (not set)
|
||||
assert.EqualValues(t, addr1, acc.GetAddress())
|
||||
assert.EqualValues(t, crypto.PubKey{}, acc.GetPubKey())
|
||||
|
||||
// can't override address
|
||||
err := acc.SetAddress(addr2)
|
||||
assert.NotNil(t, err)
|
||||
assert.EqualValues(t, addr1, acc.GetAddress())
|
||||
|
||||
// set the pubkey
|
||||
err = acc.SetPubKey(pub1)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, pub1, acc.GetPubKey())
|
||||
|
||||
// can't override pubkey
|
||||
err = acc.SetPubKey(pub2)
|
||||
assert.NotNil(t, err)
|
||||
assert.Equal(t, pub1, acc.GetPubKey())
|
||||
|
||||
//------------------------------------
|
||||
|
||||
// can set address on empty account
|
||||
acc2 := BaseAccount{}
|
||||
err = acc2.SetAddress(addr2)
|
||||
assert.Nil(t, err)
|
||||
assert.EqualValues(t, addr2, acc2.GetAddress())
|
||||
}
|
||||
|
||||
func TestBaseAccountCoins(t *testing.T) {
|
||||
_, _, addr := keyPubAddr()
|
||||
acc := NewBaseAccountWithAddress(addr)
|
||||
|
||||
someCoins := sdk.Coins{{"atom", 123}, {"eth", 246}}
|
||||
|
||||
err := acc.SetCoins(someCoins)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, someCoins, acc.GetCoins())
|
||||
}
|
||||
|
||||
func TestBaseAccountSequence(t *testing.T) {
|
||||
_, _, addr := keyPubAddr()
|
||||
acc := NewBaseAccountWithAddress(addr)
|
||||
|
||||
seq := int64(7)
|
||||
|
||||
err := acc.SetSequence(seq)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, seq, acc.GetSequence())
|
||||
}
|
||||
|
||||
func TestBaseAccountMarshal(t *testing.T) {
|
||||
_, pub, addr := keyPubAddr()
|
||||
acc := NewBaseAccountWithAddress(addr)
|
||||
|
||||
someCoins := sdk.Coins{{"atom", 123}, {"eth", 246}}
|
||||
seq := int64(7)
|
||||
|
||||
acc := NewBaseAccountWithAddress(addr)
|
||||
// set everything on the account
|
||||
err := acc.SetPubKey(pub)
|
||||
assert.Nil(t, err)
|
||||
err = acc.SetSequence(seq)
|
||||
assert.Nil(t, err)
|
||||
err = acc.SetCoins(someCoins)
|
||||
assert.Nil(t, err)
|
||||
|
||||
// need a codec for marshaling
|
||||
codec := wire.NewCodec()
|
||||
wire.RegisterCrypto(codec)
|
||||
|
||||
err := acc.SetPubKey(pub)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, pub, acc.GetPubKey())
|
||||
|
||||
assert.EqualValues(t, addr, acc.GetAddress())
|
||||
|
||||
err = acc.SetCoins(someCoins)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, someCoins, acc.GetCoins())
|
||||
|
||||
err = acc.SetSequence(seq)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, seq, acc.GetSequence())
|
||||
|
||||
b, err := codec.MarshalBinary(acc)
|
||||
assert.Nil(t, err)
|
||||
|
||||
var acc2 BaseAccount
|
||||
acc2 := BaseAccount{}
|
||||
err = codec.UnmarshalBinary(b, &acc2)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, acc, acc2)
|
||||
|
||||
// error on bad bytes
|
||||
acc2 = BaseAccount{}
|
||||
err = codec.UnmarshalBinary(b[:len(b)/2], &acc2)
|
||||
assert.NotNil(t, err)
|
||||
|
||||
}
|
||||
|
||||
func TestBaseAccountGetSet(t *testing.T) {
|
||||
_, _, addr := keyPubAddr()
|
||||
acc := NewBaseAccountWithAddress(addr)
|
||||
|
||||
// Get/Set are not yet defined - all values cause a panic.
|
||||
assert.Panics(t, func() { acc.Get("key") })
|
||||
assert.Panics(t, func() { acc.Set("key", "value") })
|
||||
}
|
||||
|
|
|
@ -16,10 +16,10 @@ import (
|
|||
|
||||
// GetAccountCmd for the auth.BaseAccount type
|
||||
func GetAccountCmdDefault(storeName string, cdc *wire.Codec) *cobra.Command {
|
||||
return GetAccountCmd(storeName, cdc, getParseAccount(cdc))
|
||||
return GetAccountCmd(storeName, cdc, GetParseAccount(cdc))
|
||||
}
|
||||
|
||||
func getParseAccount(cdc *wire.Codec) sdk.ParseAccount {
|
||||
func GetParseAccount(cdc *wire.Codec) sdk.ParseAccount {
|
||||
return func(accBytes []byte) (sdk.Account, error) {
|
||||
acct := new(auth.BaseAccount)
|
||||
err := cdc.UnmarshalBinary(accBytes, &acct)
|
||||
|
|
|
@ -38,5 +38,9 @@ func WithSigners(ctx types.Context, accounts []types.Account) types.Context {
|
|||
}
|
||||
|
||||
func GetSigners(ctx types.Context) []types.Account {
|
||||
return ctx.Value(contextKeySigners).([]types.Account)
|
||||
v := ctx.Value(contextKeySigners)
|
||||
if v == nil {
|
||||
return []types.Account{}
|
||||
}
|
||||
return v.([]types.Account)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
abci "github.com/tendermint/abci/types"
|
||||
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
)
|
||||
|
||||
func TestContextWithSigners(t *testing.T) {
|
||||
ms, _ := setupMultiStore()
|
||||
ctx := sdk.NewContext(ms, abci.Header{ChainID: "mychainid"}, false, nil)
|
||||
|
||||
_, _, addr1 := keyPubAddr()
|
||||
_, _, addr2 := keyPubAddr()
|
||||
acc1 := NewBaseAccountWithAddress(addr1)
|
||||
acc1.SetSequence(7132)
|
||||
acc2 := NewBaseAccountWithAddress(addr2)
|
||||
acc2.SetSequence(8821)
|
||||
|
||||
// new ctx has no signers
|
||||
signers := GetSigners(ctx)
|
||||
assert.Equal(t, 0, len(signers))
|
||||
|
||||
ctx2 := WithSigners(ctx, []sdk.Account{&acc1, &acc2})
|
||||
|
||||
// original context is unchanged
|
||||
signers = GetSigners(ctx)
|
||||
assert.Equal(t, 0, len(signers))
|
||||
|
||||
// new context has signers
|
||||
signers = GetSigners(ctx2)
|
||||
assert.Equal(t, 2, len(signers))
|
||||
assert.Equal(t, acc1, *(signers[0].(*BaseAccount)))
|
||||
assert.Equal(t, acc2, *(signers[1].(*BaseAccount)))
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
package rest
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/client/builder"
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
"github.com/cosmos/cosmos-sdk/wire"
|
||||
)
|
||||
|
||||
type commander struct {
|
||||
storeName string
|
||||
cdc *wire.Codec
|
||||
parser sdk.ParseAccount
|
||||
}
|
||||
|
||||
func QueryAccountRequestHandler(storeName string, cdc *wire.Codec, parser sdk.ParseAccount) func(http.ResponseWriter, *http.Request) {
|
||||
c := commander{storeName, cdc, parser}
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
addr := vars["address"]
|
||||
|
||||
bz, err := hex.DecodeString(addr)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
w.Write([]byte(err.Error()))
|
||||
return
|
||||
}
|
||||
key := sdk.Address(bz)
|
||||
|
||||
res, err := builder.Query(key, c.storeName)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Write([]byte(fmt.Sprintf("Could't query account. Error: %s", err.Error())))
|
||||
return
|
||||
}
|
||||
|
||||
// the query will return empty if there is no data for this account
|
||||
if len(res) == 0 {
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
return
|
||||
}
|
||||
|
||||
// parse out the value
|
||||
account, err := c.parser(res)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Write([]byte(fmt.Sprintf("Could't parse query result. Result: %s. Error: %s", res, err.Error())))
|
||||
return
|
||||
}
|
||||
|
||||
// print out whole account
|
||||
output, err := json.MarshalIndent(account, "", " ")
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Write([]byte(fmt.Sprintf("Could't marshall query result. Error: %s", err.Error())))
|
||||
return
|
||||
}
|
||||
|
||||
w.Write(output)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
package rest
|
||||
|
||||
import (
|
||||
"github.com/cosmos/cosmos-sdk/wire"
|
||||
auth "github.com/cosmos/cosmos-sdk/x/auth/commands"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
func RegisterRoutes(r *mux.Router, cdc *wire.Codec, storeName string) {
|
||||
r.HandleFunc("/accounts/{address}", QueryAccountRequestHandler(storeName, cdc, auth.GetParseAccount(cdc))).Methods("GET")
|
||||
}
|
|
@ -7,11 +7,12 @@ import (
|
|||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"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"
|
||||
cryptokeys "github.com/tendermint/go-crypto/keys"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -20,8 +21,8 @@ const (
|
|||
)
|
||||
|
||||
// SendTxCommand will create a send tx and sign it with the given key
|
||||
func SendTxCmd(cdc *wire.Codec) *cobra.Command {
|
||||
cmdr := commander{cdc}
|
||||
func SendTxCmd(Cdc *wire.Codec) *cobra.Command {
|
||||
cmdr := Commander{Cdc}
|
||||
cmd := &cobra.Command{
|
||||
Use: "send",
|
||||
Short: "Create and sign a send tx",
|
||||
|
@ -32,26 +33,48 @@ func SendTxCmd(cdc *wire.Codec) *cobra.Command {
|
|||
return cmd
|
||||
}
|
||||
|
||||
type commander struct {
|
||||
cdc *wire.Codec
|
||||
type Commander struct {
|
||||
Cdc *wire.Codec
|
||||
}
|
||||
|
||||
func (c commander) sendTxCmd(cmd *cobra.Command, args []string) error {
|
||||
|
||||
func (c Commander) sendTxCmd(cmd *cobra.Command, args []string) error {
|
||||
// get the from address
|
||||
from, err := builder.GetFromAddress()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// build send msg
|
||||
msg, err := buildMsg(from)
|
||||
// parse coins
|
||||
amount := viper.GetString(flagAmount)
|
||||
coins, err := sdk.ParseCoins(amount)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// parse destination address
|
||||
dest := viper.GetString(flagTo)
|
||||
bz, err := hex.DecodeString(dest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
to := sdk.Address(bz)
|
||||
|
||||
// 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(msg, c.cdc)
|
||||
res, err := builder.SignBuildBroadcast(name, passphrase, msg, c.Cdc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -60,25 +83,35 @@ func (c commander) sendTxCmd(cmd *cobra.Command, args []string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func buildMsg(from sdk.Address) (sdk.Msg, error) {
|
||||
|
||||
// parse coins
|
||||
amount := viper.GetString(flagAmount)
|
||||
coins, err := sdk.ParseCoins(amount)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// parse destination address
|
||||
dest := viper.GetString(flagTo)
|
||||
bz, err := hex.DecodeString(dest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
to := sdk.Address(bz)
|
||||
|
||||
func BuildMsg(from sdk.Address, to sdk.Address, coins sdk.Coins) sdk.Msg {
|
||||
input := bank.NewInput(from, coins)
|
||||
output := bank.NewOutput(to, coins)
|
||||
msg := bank.NewSendMsg([]bank.Input{input}, []bank.Output{output})
|
||||
return msg, nil
|
||||
return msg
|
||||
}
|
||||
|
||||
func (c Commander) SignMessage(msg sdk.Msg, kb cryptokeys.Keybase, accountName string, password string) ([]byte, error) {
|
||||
// sign and build
|
||||
bz := msg.GetSignBytes()
|
||||
sig, pubkey, err := kb.Sign(accountName, password, bz)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sigs := []sdk.StdSignature{{
|
||||
PubKey: pubkey,
|
||||
Signature: sig,
|
||||
Sequence: viper.GetInt64(client.FlagName),
|
||||
}}
|
||||
|
||||
// TODO: fees
|
||||
var fee sdk.StdFee
|
||||
|
||||
// marshal bytes
|
||||
tx := sdk.NewStdTx(msg, fee, sigs)
|
||||
|
||||
txBytes, err := c.Cdc.MarshalBinary(tx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return txBytes, nil
|
||||
}
|
||||
|
|
|
@ -20,7 +20,7 @@ func NewCoinKeeper(am sdk.AccountMapper) CoinKeeper {
|
|||
func (ck CoinKeeper) SubtractCoins(ctx sdk.Context, addr sdk.Address, amt sdk.Coins) (sdk.Coins, sdk.Error) {
|
||||
acc := ck.am.GetAccount(ctx, addr)
|
||||
if acc == nil {
|
||||
return amt, sdk.ErrUnrecognizedAddress(addr)
|
||||
return amt, sdk.ErrUnrecognizedAddress(addr.String())
|
||||
}
|
||||
|
||||
coins := acc.GetCoins()
|
||||
|
|
|
@ -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("/accounts/{address}/send", SendRequestHandler(cdc, kb)).Methods("POST")
|
||||
}
|
|
@ -0,0 +1,102 @@
|
|||
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"
|
||||
)
|
||||
|
||||
type sendBody struct {
|
||||
// fees is not used currently
|
||||
// Fees sdk.Coin `json="fees"`
|
||||
Amount sdk.Coins `json:"amount"`
|
||||
LocalAccountName string `json:"name"`
|
||||
Password string `json:"password"`
|
||||
ChainID string `json:"chain_id"`
|
||||
Sequence int64 `json:"sequence"`
|
||||
}
|
||||
|
||||
// SendRequestHandler - http request handler to send coins to a address
|
||||
func SendRequestHandler(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)
|
||||
address := vars["address"]
|
||||
|
||||
var m sendBody
|
||||
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
|
||||
msg := commands.BuildMsg(info.PubKey.Address(), to, m.Amount)
|
||||
if err != nil { // XXX rechecking same error ?
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Write([]byte(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue