diff --git a/app/app.go b/app/app.go index 69052a0ee3..d8c2f9b609 100644 --- a/app/app.go +++ b/app/app.go @@ -1,6 +1,9 @@ package app import ( + "fmt" + "strings" + abci "github.com/tendermint/abci/types" "github.com/tendermint/basecoin" eyes "github.com/tendermint/merkleeyes/client" @@ -15,8 +18,8 @@ import ( ) const ( - PluginNameBase = "base" - ChainKey = "base/chain_id" + ModuleNameBase = "base" + ChainKey = "chain_id" ) type Basecoin struct { @@ -66,18 +69,26 @@ func (app *Basecoin) Info() abci.ResponseInfo { // ABCI::SetOption func (app *Basecoin) SetOption(key string, value string) string { - if key == ChainKey { - app.state.SetChainID(value) - return "Success" + module, prefix := splitKey(key) + if module == ModuleNameBase { + return app.setBaseOption(prefix, value) } - log, err := app.handler.SetOption(app.logger, app.state, key, value) + log, err := app.handler.SetOption(app.logger, app.state, module, prefix, value) if err == nil { return log } return "Error: " + err.Error() } +func (app *Basecoin) setBaseOption(key, value string) string { + if key == ChainKey { + app.state.SetChainID(value) + return "Success" + } + return fmt.Sprintf("Error: unknown base option: %s", key) +} + // ABCI::DeliverTx func (app *Basecoin) DeliverTx(txBytes []byte) abci.Result { tx, err := basecoin.LoadTx(txBytes) @@ -178,3 +189,13 @@ func (app *Basecoin) EndBlock(height uint64) (res abci.ResponseEndBlock) { // } return } + +// Splits the string at the first '/'. +// if there are none, assign default module ("base"). +func splitKey(key string) (string, string) { + if strings.Contains(key, "/") { + keyParts := strings.SplitN(key, "/", 2) + return keyParts[0], keyParts[1] + } + return ModuleNameBase, key +} diff --git a/app/app_test.go b/app/app_test.go index ab007ce374..a86f290024 100644 --- a/app/app_test.go +++ b/app/app_test.go @@ -58,7 +58,7 @@ func (at *appTest) getTx(seq int, coins types.Coins) basecoin.Tx { func (at *appTest) acc2app(acc types.Account) { accBytes, err := json.Marshal(acc) require.Nil(at.t, err) - res := at.app.SetOption("base/account", string(accBytes)) + res := at.app.SetOption("coin/account", string(accBytes)) require.EqualValues(at.t, res, "Success") } @@ -137,7 +137,7 @@ func TestSetOption(t *testing.T) { accIn := types.MakeAcc("input0").Account accsInBytes, err := json.Marshal(accIn) assert.Nil(err) - res = app.SetOption("base/account", string(accsInBytes)) + res = app.SetOption("coin/account", string(accsInBytes)) require.EqualValues(res, "Success") // make sure it is set correctly, with some balance @@ -165,7 +165,7 @@ func TestSetOption(t *testing.T) { } ] }` - res = app.SetOption("base/account", unsortAcc) + res = app.SetOption("coin/account", unsortAcc) require.EqualValues(res, "Success") coins, err = getAddr(unsortAddr, app.state) @@ -234,3 +234,19 @@ func TestQuery(t *testing.T) { }) assert.NotEqual(resQueryPreCommit, resQueryPostCommit, "Query should change before/after commit") } + +func TestSplitKey(t *testing.T) { + assert := assert.New(t) + prefix, suffix := splitKey("foo/bar") + assert.EqualValues("foo", prefix) + assert.EqualValues("bar", suffix) + + prefix, suffix = splitKey("foobar") + assert.EqualValues("base", prefix) + assert.EqualValues("foobar", suffix) + + prefix, suffix = splitKey("some/complex/issue") + assert.EqualValues("some", prefix) + assert.EqualValues("complex/issue", suffix) + +} diff --git a/app/genesis.go b/app/genesis.go index 05913c15b0..9d6050bcdb 100644 --- a/app/genesis.go +++ b/app/genesis.go @@ -19,7 +19,7 @@ func (app *Basecoin) LoadGenesis(path string) error { // set accounts for _, acct := range genDoc.AppOptions.Accounts { - _ = app.SetOption("base/account", string(acct)) + _ = app.SetOption("coin/account", string(acct)) } // set plugin options diff --git a/errors/common.go b/errors/common.go index fdc04ea8ad..ac9e2809a8 100644 --- a/errors/common.go +++ b/errors/common.go @@ -24,6 +24,7 @@ var ( errWrongChain = rawerr.New("Wrong chain for tx") errUnknownTxType = rawerr.New("Tx type unknown") errInvalidFormat = rawerr.New("Invalid format") + errUnknownModule = rawerr.New("Unknown module") ) func ErrUnknownTxType(tx basecoin.Tx) TMError { @@ -44,6 +45,14 @@ func IsInvalidFormatErr(err error) bool { return IsSameError(errInvalidFormat, err) } +func ErrUnknownModule(mod string) TMError { + w := errors.Wrap(errUnknownModule, mod) + return WithCode(w, abci.CodeType_UnknownRequest) +} +func IsUnknownModuleErr(err error) bool { + return IsSameError(errUnknownModule, err) +} + func ErrInternal(msg string) TMError { return New(msg, abci.CodeType_InternalError) } diff --git a/handler.go b/handler.go index 3e3728a09b..34404395b5 100644 --- a/handler.go +++ b/handler.go @@ -47,14 +47,14 @@ func (c DeliverFunc) DeliverTx(ctx Context, store types.KVStore, tx Tx) (Result, } type SetOptioner interface { - SetOption(l log.Logger, store types.KVStore, key, value string) (string, error) + SetOption(l log.Logger, store types.KVStore, module, key, value string) (string, error) } // SetOptionFunc (like http.HandlerFunc) is a shortcut for making wrapers -type SetOptionFunc func(log.Logger, types.KVStore, string, string) (string, error) +type SetOptionFunc func(log.Logger, types.KVStore, string, string, string) (string, error) -func (c SetOptionFunc) SetOption(l log.Logger, store types.KVStore, key, value string) (string, error) { - return c(l, store, key, value) +func (c SetOptionFunc) SetOption(l log.Logger, store types.KVStore, module, key, value string) (string, error) { + return c(l, store, module, key, value) } // Result captures any non-error abci result @@ -83,6 +83,6 @@ func (_ NopDeliver) DeliverTx(Context, types.KVStore, Tx) (r Result, e error) { type NopOption struct{} -func (_ NopOption) SetOption(log.Logger, types.KVStore, string, string) (string, error) { +func (_ NopOption) SetOption(log.Logger, types.KVStore, string, string, string) (string, error) { return "", nil } diff --git a/modules/coin/handler.go b/modules/coin/handler.go index 63cf3d693b..def6689c29 100644 --- a/modules/coin/handler.go +++ b/modules/coin/handler.go @@ -80,8 +80,11 @@ func (h Handler) DeliverTx(ctx basecoin.Context, store types.KVStore, tx basecoi return basecoin.Result{}, nil } -func (h Handler) SetOption(l log.Logger, store types.KVStore, key, value string) (log string, err error) { - if key == "base/account" { +func (h Handler) SetOption(l log.Logger, store types.KVStore, module, key, value string) (log string, err error) { + if module != NameCoin { + return "", errors.ErrUnknownModule(module) + } + if key == "account" { var acc GenesisAccount err = data.FromJSON([]byte(value), &acc) if err != nil { diff --git a/modules/coin/handler_test.go b/modules/coin/handler_test.go index 534b5ee168..b13d1bb59a 100644 --- a/modules/coin/handler_test.go +++ b/modules/coin/handler_test.go @@ -172,8 +172,7 @@ func TestSetOption(t *testing.T) { // some sample settings pk := crypto.GenPrivKeySecp256k1().Wrap() addr := pk.PubKey().Address() - actor := basecoin.Actor{App: "coin", Address: addr} - // actor2 := basecoin.Actor{App: "foo", Address: addr} + actor := basecoin.Actor{App: stack.NameSigs, Address: addr} someCoins := types.Coins{{"atom", 123}} otherCoins := types.Coins{{"eth", 11}} @@ -198,13 +197,13 @@ func TestSetOption(t *testing.T) { l := log.NewNopLogger() for i, tc := range cases { store := types.NewMemKVStore() - key := "base/account" + key := "account" // set the options for j, gen := range tc.init { value, err := json.Marshal(gen) require.Nil(err, "%d,%d: %+v", i, j, err) - _, err = h.SetOption(l, store, key, string(value)) + _, err = h.SetOption(l, store, NameCoin, key, string(value)) require.Nil(err) } diff --git a/modules/coin/store.go b/modules/coin/store.go index 6170f780a6..9f9f671104 100644 --- a/modules/coin/store.go +++ b/modules/coin/store.go @@ -94,6 +94,7 @@ type Account struct { } func loadAccount(store types.KVStore, key []byte) (acct Account, err error) { + // fmt.Printf("load: %X\n", key) data := store.Get(key) if len(data) == 0 { return acct, ErrNoAccount() @@ -107,6 +108,7 @@ func loadAccount(store types.KVStore, key []byte) (acct Account, err error) { } func storeAccount(store types.KVStore, key []byte, acct Account) error { + // fmt.Printf("store: %X\n", key) bin := wire.BinaryBytes(acct) store.Set(key, bin) return nil // real stores can return error... diff --git a/stack/dispatcher.go b/stack/dispatcher.go new file mode 100644 index 0000000000..7c07266b37 --- /dev/null +++ b/stack/dispatcher.go @@ -0,0 +1,94 @@ +package stack + +import ( + "fmt" + + "github.com/tendermint/tmlibs/log" + + "github.com/tendermint/basecoin" + "github.com/tendermint/basecoin/errors" + "github.com/tendermint/basecoin/types" +) + +const ( + NameDispatcher = "disp" +) + +// Dispatcher grabs a bunch of Dispatchables and groups them into one Handler. +// +// It will route tx to the proper locations and also allows them to call each +// other synchronously through the same tx methods. +type Dispatcher struct { + routes map[string]Dispatchable +} + +func NewDispatcher(routes ...Dispatchable) *Dispatcher { + d := &Dispatcher{} + d.AddRoutes(routes...) + return d +} + +var _ basecoin.Handler = new(Dispatcher) + +// AddRoutes registers all these dispatchable choices under their subdomains +// +// Panics on attempt to double-register a route name, as this is a configuration error. +// Should I retrun an error instead? +func (d *Dispatcher) AddRoutes(routes ...Dispatchable) { + for _, r := range routes { + name := r.Name() + if _, ok := d.routes[name]; ok { + panic(fmt.Sprintf("%s already registered with dispatcher", name)) + } + d.routes[name] = r + } +} + +func (d *Dispatcher) Name() string { + return NameDispatcher +} + +func (d *Dispatcher) CheckTx(ctx basecoin.Context, store types.KVStore, tx basecoin.Tx) (res basecoin.Result, err error) { + r, err := d.lookupTx(tx) + if err != nil { + return res, err + } + // TODO: callback + return r.CheckTx(ctx, store, tx, nil) +} + +func (d *Dispatcher) DeliverTx(ctx basecoin.Context, store types.KVStore, tx basecoin.Tx) (res basecoin.Result, err error) { + r, err := d.lookupTx(tx) + if err != nil { + return res, err + } + // TODO: callback + return r.DeliverTx(ctx, store, tx, nil) +} + +func (d *Dispatcher) SetOption(l log.Logger, store types.KVStore, module, key, value string) (string, error) { + r, err := d.lookupModule(module) + if err != nil { + return "", err + } + // TODO: callback + return r.SetOption(l, store, module, key, value, nil) +} + +func (d *Dispatcher) lookupTx(tx basecoin.Tx) (Dispatchable, error) { + // TODO + name := "foo" + r, ok := d.routes[name] + if !ok { + return nil, errors.ErrUnknownTxType(tx) + } + return r, nil +} + +func (d *Dispatcher) lookupModule(name string) (Dispatchable, error) { + r, ok := d.routes[name] + if !ok { + return nil, errors.ErrUnknownModule(name) + } + return r, nil +} diff --git a/stack/interface.go b/stack/interface.go index 951a7fa73f..09e1ad4d16 100644 --- a/stack/interface.go +++ b/stack/interface.go @@ -39,13 +39,13 @@ func (d DeliverMiddleFunc) DeliverTx(ctx basecoin.Context, store types.KVStore, } type SetOptionMiddle interface { - SetOption(l log.Logger, store types.KVStore, key, value string, next basecoin.SetOptioner) (string, error) + SetOption(l log.Logger, store types.KVStore, module, key, value string, next basecoin.SetOptioner) (string, error) } -type SetOptionMiddleFunc func(log.Logger, types.KVStore, string, string, basecoin.SetOptioner) (string, error) +type SetOptionMiddleFunc func(log.Logger, types.KVStore, string, string, string, basecoin.SetOptioner) (string, error) -func (c SetOptionMiddleFunc) SetOption(l log.Logger, store types.KVStore, key, value string, next basecoin.SetOptioner) (string, error) { - return c(l, store, key, value, next) +func (c SetOptionMiddleFunc) SetOption(l log.Logger, store types.KVStore, module, key, value string, next basecoin.SetOptioner) (string, error) { + return c(l, store, module, key, value, next) } // holders @@ -63,6 +63,43 @@ func (_ PassDeliver) DeliverTx(ctx basecoin.Context, store types.KVStore, tx bas type PassOption struct{} -func (_ PassOption) SetOption(l log.Logger, store types.KVStore, key, value string, next basecoin.SetOptioner) (string, error) { - return next.SetOption(l, store, key, value) +func (_ PassOption) SetOption(l log.Logger, store types.KVStore, module, key, value string, next basecoin.SetOptioner) (string, error) { + return next.SetOption(l, store, module, key, value) +} + +// Dispatchable is like middleware, except the meaning of "next" is different. +// Whereas in the middleware, it is the next handler that we should pass the same tx into, +// for dispatchers, it is a dispatcher, which it can use to +type Dispatchable interface { + Middleware + AssertDispatcher() +} + +// WrapHandler turns a basecoin.Handler into a Dispatchable interface +func WrapHandler(h basecoin.Handler) Dispatchable { + return wrapped{h} +} + +type wrapped struct { + h basecoin.Handler +} + +var _ Dispatchable = wrapped{} + +func (w wrapped) AssertDispatcher() {} + +func (w wrapped) Name() string { + return w.h.Name() +} + +func (w wrapped) CheckTx(ctx basecoin.Context, store types.KVStore, tx basecoin.Tx, _ basecoin.Checker) (basecoin.Result, error) { + return w.h.CheckTx(ctx, store, tx) +} + +func (w wrapped) DeliverTx(ctx basecoin.Context, store types.KVStore, tx basecoin.Tx, _ basecoin.Deliver) (basecoin.Result, error) { + return w.h.DeliverTx(ctx, store, tx) +} + +func (w wrapped) SetOption(l log.Logger, store types.KVStore, module, key, value string, _ basecoin.SetOptioner) (string, error) { + return w.h.SetOption(l, store, module, key, value) } diff --git a/stack/logger.go b/stack/logger.go index 2bd41a5fa2..1f79ac3d36 100644 --- a/stack/logger.go +++ b/stack/logger.go @@ -50,12 +50,12 @@ func (_ Logger) DeliverTx(ctx basecoin.Context, store types.KVStore, tx basecoin return } -func (_ Logger) SetOption(l log.Logger, store types.KVStore, key, value string, next basecoin.SetOptioner) (string, error) { +func (_ Logger) SetOption(l log.Logger, store types.KVStore, module, key, value string, next basecoin.SetOptioner) (string, error) { start := time.Now() - res, err := next.SetOption(l, store, key, value) + res, err := next.SetOption(l, store, module, key, value) delta := time.Now().Sub(start) // TODO: log the value being set also? - l = l.With("duration", micros(delta)).With("key", key) + l = l.With("duration", micros(delta)).With("mod", module).With("key", key) if err == nil { l.Info("SetOption", "log", res) } else { diff --git a/stack/middleware.go b/stack/middleware.go index fcc450b730..ed9c3beba0 100644 --- a/stack/middleware.go +++ b/stack/middleware.go @@ -39,8 +39,8 @@ func (m *middleware) DeliverTx(ctx basecoin.Context, store types.KVStore, tx bas return m.middleware.DeliverTx(ctx, store, tx, next) } -func (m *middleware) SetOption(l log.Logger, store types.KVStore, key, value string) (string, error) { - return m.middleware.SetOption(l, store, key, value, m.next) +func (m *middleware) SetOption(l log.Logger, store types.KVStore, module, key, value string) (string, error) { + return m.middleware.SetOption(l, store, module, key, value, m.next) } // Stack is the entire application stack diff --git a/stack/recovery.go b/stack/recovery.go index 7dbb10a439..be213ad878 100644 --- a/stack/recovery.go +++ b/stack/recovery.go @@ -41,13 +41,13 @@ func (_ Recovery) DeliverTx(ctx basecoin.Context, store types.KVStore, tx baseco return next.DeliverTx(ctx, store, tx) } -func (_ Recovery) SetOption(l log.Logger, store types.KVStore, key, value string, next basecoin.SetOptioner) (log string, err error) { +func (_ Recovery) SetOption(l log.Logger, store types.KVStore, module, key, value string, next basecoin.SetOptioner) (log string, err error) { defer func() { if r := recover(); r != nil { err = normalizePanic(r) } }() - return next.SetOption(l, store, key, value) + return next.SetOption(l, store, module, key, value) } // normalizePanic makes sure we can get a nice TMError (with stack) out of it