From 9a98c2176fb8219819374f0d1b149db03b73b07b Mon Sep 17 00:00:00 2001 From: Daniil Lashin Date: Wed, 28 Nov 2018 18:03:42 +0300 Subject: [PATCH 01/49] Add log_format option to config --- CHANGELOG.md | 7 +++++++ config/config.go | 12 ++++++++++++ config/toml.go | 3 +++ docker-compose.yml | 2 +- version/version.go | 4 ++-- 5 files changed, 25 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 54e4fab86..ba453cbad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## 0.7.7 +*Nov 28th, 2018* + +IMPROVEMENT + +- [logs] Add `log_format` option to config + ## 0.7.6 *Nov 27th, 2018* diff --git a/config/config.go b/config/config.go index 7f31ee86f..27ae2a062 100644 --- a/config/config.go +++ b/config/config.go @@ -26,6 +26,13 @@ var ( defaultNodeKeyPath = filepath.Join(defaultConfigDir, defaultNodeKeyName) ) +const ( + // LogFormatPlain is a format for colored text + LogFormatPlain = "plain" + // LogFormatJSON is a format for json output + LogFormatJSON = "json" +) + func init() { homeDir := utils.GetMinterHome() viper.SetConfigName("config") @@ -150,6 +157,7 @@ func GetTmConfig() *tmConfig.Config { ProxyApp: cfg.ProxyApp, ABCI: cfg.ABCI, LogLevel: cfg.LogLevel, + LogFormat: cfg.LogFormat, ProfListenAddress: cfg.ProfListenAddress, FastSync: cfg.FastSync, FilterPeers: cfg.FilterPeers, @@ -203,6 +211,9 @@ type BaseConfig struct { // Output level for logging LogLevel string `mapstructure:"log_level"` + // Output format: 'plain' (colored text) or 'json' + LogFormat string `mapstructure:"log_format"` + // TCP or UNIX socket address for the profiling server to listen on ProfListenAddress string `mapstructure:"prof_laddr"` @@ -263,6 +274,7 @@ func DefaultBaseConfig() BaseConfig { APIPerIPLimit: 1000, APIPerIPLimitWindow: 60 * time.Second, LogPath: "stdout", + LogFormat: LogFormatPlain, } } diff --git a/config/toml.go b/config/toml.go index cbcb68d9d..9094c1db6 100644 --- a/config/toml.go +++ b/config/toml.go @@ -102,6 +102,9 @@ db_path = "{{ js .BaseConfig.DBPath }}" # Output level for logging, including package level options log_level = "{{ .BaseConfig.LogLevel }}" +# Output format: 'plain' (colored text) or 'json' +log_format = "{{ .BaseConfig.LogFormat }}" + # Path to file for logs, "stdout" by default log_path = "{{ .BaseConfig.LogPath }}" diff --git a/docker-compose.yml b/docker-compose.yml index b7388dad3..eee078e9c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,7 +1,7 @@ version: "3.4" services: minter: - image: minterteam/minter:0.7.6 + image: minterteam/minter:0.7.7 volumes: - ~/.minter:/minter ports: diff --git a/version/version.go b/version/version.go index 85bad2ff9..f992e9c68 100755 --- a/version/version.go +++ b/version/version.go @@ -4,12 +4,12 @@ package version const ( Maj = "0" Min = "7" - Fix = "6" + Fix = "7" ) var ( // Must be a string because scripts like dist.sh read this file. - Version = "0.7.6" + Version = "0.7.7" // GitCommit is the current HEAD set using ldflags. GitCommit string From 15ac5a4cda0178bcf3619ee9c0b40bc0e5833d65 Mon Sep 17 00:00:00 2001 From: Daniil Lashin Date: Thu, 29 Nov 2018 13:55:43 +0300 Subject: [PATCH 02/49] Switch to RPC protocol. Closes #129, #163 --- CHANGELOG.md | 4 + api/address.go | 33 ++++++ api/api.go | 194 ++++++++++------------------------ api/balance.go | 55 ---------- api/block.go | 75 ++----------- api/candidate.go | 44 ++------ api/candidates.go | 30 +----- api/coin_info.go | 56 +++------- api/estimate_coin_buy.go | 98 +++++------------ api/estimate_coin_sell.go | 98 +++++------------ api/estimate_tx_commission.go | 49 ++------- api/net_info.go | 24 +---- api/send_transaction.go | 64 +---------- api/status.go | 23 +--- api/transaction.go | 76 +++++-------- api/transaction_count.go | 39 ------- api/transactions.go | 28 +---- api/unconfirmed_txs.go | 10 ++ api/validators.go | 61 ++++------- config/config.go | 6 +- config/toml.go | 6 -- core/types/types.go | 15 +++ 22 files changed, 269 insertions(+), 819 deletions(-) create mode 100644 api/address.go delete mode 100644 api/balance.go delete mode 100644 api/transaction_count.go create mode 100644 api/unconfirmed_txs.go diff --git a/CHANGELOG.md b/CHANGELOG.md index ba453cbad..e9e08944b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,10 @@ ## 0.7.7 *Nov 28th, 2018* +BREAKING CHANGES + +- [api] Switch to RPC protocol + IMPROVEMENT - [logs] Add `log_format` option to config diff --git a/api/address.go b/api/address.go new file mode 100644 index 000000000..3f9e01dd4 --- /dev/null +++ b/api/address.go @@ -0,0 +1,33 @@ +package api + +import ( + "github.com/MinterTeam/minter-go-node/core/types" +) + +type AddressResponse struct { + Balance map[string]string `json:"balance"` + TransactionCount uint64 `json:"transaction_count"` +} + +func Address(address types.Address, height int) (*AddressResponse, error) { + cState, err := GetStateForHeight(height) + if err != nil { + return nil, err + } + + response := AddressResponse{ + Balance: make(map[string]string), + TransactionCount: cState.GetNonce(address), + } + balances := cState.GetBalances(address) + + for k, v := range balances.Data { + response.Balance[k.String()] = v.String() + } + + if _, exists := response.Balance[types.GetBaseCoin().String()]; !exists { + response.Balance[types.GetBaseCoin().String()] = "0" + } + + return &response, nil +} diff --git a/api/api.go b/api/api.go index 2bddc8170..0894629bb 100644 --- a/api/api.go +++ b/api/api.go @@ -1,33 +1,28 @@ package api import ( - "encoding/json" + "fmt" "github.com/MinterTeam/minter-go-node/config" + "github.com/MinterTeam/minter-go-node/core/minter" + "github.com/MinterTeam/minter-go-node/core/state" "github.com/MinterTeam/minter-go-node/eventsdb" "github.com/MinterTeam/minter-go-node/log" - "github.com/gorilla/mux" "github.com/rs/cors" - "net/http" - "strings" - "sync" - "sync/atomic" - - "github.com/MinterTeam/minter-go-node/core/minter" - "github.com/MinterTeam/minter-go-node/core/state" "github.com/tendermint/go-amino" "github.com/tendermint/tendermint/crypto/encoding/amino" rpc "github.com/tendermint/tendermint/rpc/client" - "strconv" + "github.com/tendermint/tendermint/rpc/lib/server" + "net/http" + "net/url" + "strings" "time" ) var ( - cdc = amino.NewCodec() - blockchain *minter.Blockchain - client *rpc.Local - connections = int32(0) - limitter = make(chan struct{}, 10) - cfg = config.GetConfig() + cdc = amino.NewCodec() + blockchain *minter.Blockchain + client *rpc.Local + cfg = config.GetConfig() ) func init() { @@ -35,146 +30,73 @@ func init() { eventsdb.RegisterAminoEvents(cdc) } +var Routes = map[string]*rpcserver.RPCFunc{ + "status": rpcserver.NewRPCFunc(Status, ""), + "candidates": rpcserver.NewRPCFunc(Candidates, "height"), + "candidate": rpcserver.NewRPCFunc(Candidate, "pubkey,height"), + "validators": rpcserver.NewRPCFunc(Validators, "height"), + "address": rpcserver.NewRPCFunc(Address, "address,height"), + "send_transaction": rpcserver.NewRPCFunc(SendTransaction, "tx"), + "transaction": rpcserver.NewRPCFunc(Transaction, "hash"), + "transactions": rpcserver.NewRPCFunc(Transactions, "query"), + "block": rpcserver.NewRPCFunc(Block, "height"), + "net_info": rpcserver.NewRPCFunc(NetInfo, ""), + "coin_info": rpcserver.NewRPCFunc(CoinInfo, "symbol,height"), + "estimate_coin_sell": rpcserver.NewRPCFunc(EstimateCoinSell, "coin_to_sell,coin_to_buy,value_to_sell,height"), + "estimate_coin_buy": rpcserver.NewRPCFunc(EstimateCoinBuy, "coin_to_sell,coin_to_buy,value_to_buy,height"), + "estimate_tx_commission": rpcserver.NewRPCFunc(EstimateTxCommission, "tx"), + "unconfirmed_txs": rpcserver.NewRPCFunc(UnconfirmedTxs, "limit"), +} + func RunApi(b *minter.Blockchain, tmRPC *rpc.Local) { client = tmRPC - blockchain = b + waitForTendermint() - router := mux.NewRouter().StrictSlash(true) + m := http.NewServeMux() + logger := log.With("module", "rpc") + rpcserver.RegisterRPCFuncs(m, Routes, cdc, logger) + listener, err := rpcserver.Listen(config.GetConfig().APIListenAddress, rpcserver.Config{ + MaxOpenConnections: cfg.APISimultaneousRequests, + }) - stats := IpStats{ - ips: make(map[string]int), - lock: sync.Mutex{}, + if err != nil { + panic(err) } - router.Use(RateLimit(cfg.APIPerIPLimit, cfg.APIPerIPLimitWindow, &stats)) - - router.HandleFunc("/api/candidates", wrapper(GetCandidates)).Methods("GET") - router.HandleFunc("/api/candidate/{pubkey}", wrapper(GetCandidate)).Methods("GET") - router.HandleFunc("/api/validators", wrapper(GetValidators)).Methods("GET") - router.HandleFunc("/api/balance/{address}", wrapper(GetBalance)).Methods("GET") - router.HandleFunc("/api/transactionCount/{address}", wrapper(GetTransactionCount)).Methods("GET") - router.HandleFunc("/api/sendTransaction", wrapper(SendTransaction)).Methods("POST") - router.HandleFunc("/api/transaction/{hash}", wrapper(Transaction)).Methods("GET") - router.HandleFunc("/api/block/{height}", wrapper(Block)).Methods("GET") - router.HandleFunc("/api/transactions", wrapper(Transactions)).Methods("GET") - router.HandleFunc("/api/status", wrapper(Status)).Methods("GET") - router.HandleFunc("/api/net_info", wrapper(NetInfo)).Methods("GET") - router.HandleFunc("/api/coinInfo/{symbol}", wrapper(GetCoinInfo)).Methods("GET") - router.HandleFunc("/api/estimateCoinSell", wrapper(EstimateCoinSell)).Methods("GET") - router.HandleFunc("/api/estimateCoinBuy", wrapper(EstimateCoinBuy)).Methods("GET") - router.HandleFunc("/api/estimateTxCommission", wrapper(EstimateTxCommission)).Methods("GET") - c := cors.New(cors.Options{ AllowedOrigins: []string{"*"}, AllowedMethods: []string{"POST", "GET"}, AllowCredentials: true, }) - handler := c.Handler(router) - - // wait for tendermint to start - waitForTendermint() - - log.Error("Failed to start API", "err", http.ListenAndServe(config.GetConfig().APIListenAddress, handler)) + handler := c.Handler(m) + log.Error("Failed to start API", "err", rpcserver.StartHTTPServer(listener, Handler(handler), logger)) } -func wrapper(f func(w http.ResponseWriter, r *http.Request)) func(w http.ResponseWriter, r *http.Request) { - return func(w http.ResponseWriter, r *http.Request) { - if atomic.LoadInt32(&connections) > int32(cfg.APISimultaneousRequests) { - w.Header().Set("Content-Type", "application/json; charset=UTF-8") - w.WriteHeader(http.StatusTooManyRequests) - json.NewEncoder(w).Encode(Response{ - Code: http.StatusTooManyRequests, - Log: "Too many requests", - }) - return - } - - atomic.AddInt32(&connections, 1) - limitter <- struct{}{} - - defer func() { - log.With("module", "api").Info("Served API request", "req", r.RequestURI) - <-limitter - atomic.AddInt32(&connections, -1) - }() - - f(w, r) - } -} +func Handler(h http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + query := r.URL.Query() -type IpStats struct { - ips map[string]int - lock sync.Mutex -} - -func (s *IpStats) Reset() { - s.lock.Lock() - defer s.lock.Unlock() - - s.ips = make(map[string]int) -} - -func (s *IpStats) Add(identifier string, count int) int { - s.lock.Lock() - defer s.lock.Unlock() - - s.ips[identifier] += count - - return s.ips[identifier] -} - -type Stats interface { - // Reset will reset the map. - Reset() - - // Add would add "count" to the map at the key of "identifier", - // and returns an int which is the total count of the value - // at that key. - Add(identifier string, count int) int -} - -func RateLimit(limit int, window time.Duration, stats Stats) func(next http.Handler) http.Handler { - var windowStart time.Time - - // Clear the rate limit stats after each window. - ticker := time.NewTicker(window) - go func() { - windowStart = time.Now() - - for range ticker.C { - windowStart = time.Now() - stats.Reset() - } - }() - - return func(next http.Handler) http.Handler { - h := func(w http.ResponseWriter, r *http.Request) { - value := int(stats.Add(identifyRequest(r), 1)) - - XRateLimitRemaining := limit - value - if XRateLimitRemaining < 0 { - XRateLimitRemaining = 0 + for key, value := range query { + val := value[0] + if strings.HasPrefix(val, "Mp") { + query.Set(key, fmt.Sprintf("0x%s", strings.TrimPrefix(val, "Mp"))) } - w.Header().Add("X-Rate-Limit-Limit", strconv.Itoa(limit)) - w.Header().Add("X-Rate-Limit-Remaining", strconv.Itoa(XRateLimitRemaining)) - w.Header().Add("X-Rate-Limit-Reset", strconv.Itoa(int(window.Seconds()-time.Since(windowStart).Seconds())+1)) - - if value >= limit { - w.WriteHeader(429) - } else { - next.ServeHTTP(w, r) + if strings.HasPrefix(val, "Mx") { + query.Set(key, fmt.Sprintf("\"%s\"", val)) } } - return http.HandlerFunc(h) - } -} + var err error + r.URL, err = url.ParseRequestURI(fmt.Sprintf("%s?%s", r.URL.Path, query.Encode())) + if err != nil { + panic(err) + } -func identifyRequest(r *http.Request) string { - return strings.Split(r.Header.Get("X-Real-IP"), ":")[0] + h.ServeHTTP(w, r) + }) } func waitForTendermint() { @@ -194,9 +116,7 @@ type Response struct { Log string `json:"log,omitempty"` } -func GetStateForRequest(r *http.Request) (*state.StateDB, error) { - height, _ := strconv.Atoi(r.URL.Query().Get("height")) - +func GetStateForHeight(height int) (*state.StateDB, error) { if height > 0 { cState, err := blockchain.GetStateForHeight(height) diff --git a/api/balance.go b/api/balance.go deleted file mode 100644 index edb4f8416..000000000 --- a/api/balance.go +++ /dev/null @@ -1,55 +0,0 @@ -package api - -import ( - "encoding/json" - "github.com/MinterTeam/minter-go-node/core/types" - "github.com/gorilla/mux" - "net/http" -) - -type BalanceResponse struct { - Balance map[string]string `json:"balance"` -} - -type BalanceRequest struct { - Address types.Address `json:"address"` - Coin types.CoinSymbol `json:"coin"` -} - -func GetBalance(w http.ResponseWriter, r *http.Request) { - cState, err := GetStateForRequest(r) - - if err != nil { - w.Header().Set("Content-Type", "application/json; charset=UTF-8") - w.WriteHeader(http.StatusNotFound) - _ = json.NewEncoder(w).Encode(Response{ - Code: 404, - Log: "State for given height not found", - }) - return - } - - vars := mux.Vars(r) - address := types.HexToAddress(vars["address"]) - - balanceResponse := BalanceResponse{ - Balance: make(map[string]string), - } - balances := cState.GetBalances(address) - - for k, v := range balances.Data { - balanceResponse.Balance[k.String()] = v.String() - } - - if _, exists := balanceResponse.Balance[types.GetBaseCoin().String()]; !exists { - balanceResponse.Balance[types.GetBaseCoin().String()] = "0" - } - - w.Header().Set("Content-Type", "application/json; charset=UTF-8") - w.WriteHeader(http.StatusOK) - - _ = json.NewEncoder(w).Encode(Response{ - Code: 0, - Result: balanceResponse, - }) -} diff --git a/api/block.go b/api/block.go index adbb6b5bb..1e02dad26 100644 --- a/api/block.go +++ b/api/block.go @@ -1,17 +1,14 @@ package api import ( - "encoding/json" "fmt" "github.com/MinterTeam/minter-go-node/core/rewards" "github.com/MinterTeam/minter-go-node/core/transaction" "github.com/MinterTeam/minter-go-node/core/types" "github.com/MinterTeam/minter-go-node/eventsdb" - "github.com/gorilla/mux" "github.com/tendermint/tendermint/libs/common" + tmtypes "github.com/tendermint/tendermint/types" "math/big" - "net/http" - "strconv" "time" ) @@ -28,8 +25,8 @@ type BlockResponse struct { NumTxs int64 `json:"num_txs"` TotalTxs int64 `json:"total_txs"` Transactions []BlockTransactionResponse `json:"transactions"` - Events json.RawMessage `json:"events,omitempty"` - Precommits json.RawMessage `json:"precommits"` + Events eventsdb.Events `json:"events,omitempty"` + Precommits []*tmtypes.Vote `json:"precommits"` BlockReward string `json:"block_reward"` Size int `json:"size"` } @@ -52,31 +49,14 @@ type BlockTransactionResponse struct { Log string `json:"log,omitempty"` } -func Block(w http.ResponseWriter, r *http.Request) { - - vars := mux.Vars(r) - height, _ := strconv.ParseInt(vars["height"], 10, 64) - +func Block(height int64) (*BlockResponse, error) { block, err := client.Block(&height) blockResults, err := client.BlockResults(&height) - - w.Header().Set("Content-Type", "application/json; charset=UTF-8") - if err != nil { - w.WriteHeader(http.StatusBadRequest) - err = json.NewEncoder(w).Encode(Response{ - Code: 0, - Log: err.Error(), - }) - - if err != nil { - panic(err) - } - return + return nil, err } txs := make([]BlockTransactionResponse, len(block.Block.Data.Txs)) - for i, rawTx := range block.Block.Data.Txs { tx, _ := transaction.DecodeFromBytes(rawTx) sender, _ := tx.Sender() @@ -111,51 +91,16 @@ func Block(w http.ResponseWriter, r *http.Request) { } } - precommits, _ := cdc.MarshalJSON(block.Block.LastCommit.Precommits) - - size := len(cdc.MustMarshalBinaryLengthPrefixed(block)) - - var eventsRaw []byte - - events := edb.LoadEvents(height) - - if len(events) > 0 { - eventsRaw, err = cdc.MarshalJSON(events) - - if err != nil { - w.WriteHeader(http.StatusBadRequest) - err = json.NewEncoder(w).Encode(Response{ - Code: 0, - Log: err.Error(), - }) - - if err != nil { - panic(err) - } - return - } - } - - response := BlockResponse{ + return &BlockResponse{ Hash: block.Block.Hash(), Height: block.Block.Height, Time: block.Block.Time, NumTxs: block.Block.NumTxs, TotalTxs: block.Block.TotalTxs, Transactions: txs, - Precommits: precommits, + Precommits: block.Block.LastCommit.Precommits, BlockReward: rewards.GetRewardForBlock(uint64(height)).String(), - Size: size, - Events: eventsRaw, - } - - w.WriteHeader(http.StatusOK) - err = json.NewEncoder(w).Encode(Response{ - Code: 0, - Result: response, - }) - - if err != nil { - panic(err) - } + Size: len(cdc.MustMarshalBinaryLengthPrefixed(block)), + Events: edb.LoadEvents(height), + }, nil } diff --git a/api/candidate.go b/api/candidate.go index b36653bb9..3a2a6f306 100644 --- a/api/candidate.go +++ b/api/candidate.go @@ -1,50 +1,20 @@ package api import ( - "encoding/json" - "github.com/MinterTeam/minter-go-node/core/types" - "github.com/gorilla/mux" - "net/http" - "strings" + "github.com/pkg/errors" ) -func GetCandidate(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - pubkey := types.Hex2Bytes(strings.TrimLeft(vars["pubkey"], "Mp")) - - cState, err := GetStateForRequest(r) - +func Candidate(pubkey []byte, height int) (*CandidateResponse, error) { + cState, err := GetStateForHeight(height) if err != nil { - w.Header().Set("Content-Type", "application/json; charset=UTF-8") - w.WriteHeader(http.StatusNotFound) - _ = json.NewEncoder(w).Encode(Response{ - Code: 404, - Log: "State for given height not found", - }) - return + return nil, err } candidate := cState.GetStateCandidate(pubkey) - - w.Header().Set("Content-Type", "application/json; charset=UTF-8") - if candidate == nil { - w.WriteHeader(http.StatusNotFound) - _ = json.NewEncoder(w).Encode(Response{ - Code: 404, - Log: "Candidate not found", - }) - return + return nil, errors.New("Candidate not found") } - w.WriteHeader(http.StatusOK) - - _ = json.NewEncoder(w).Encode(Response{ - Code: 0, - Result: struct { - Candidate Candidate `json:"candidate"` - }{ - Candidate: makeResponseCandidate(*candidate, true), - }, - }) + response := makeResponseCandidate(*candidate, true) + return &response, nil } diff --git a/api/candidates.go b/api/candidates.go index 0d93e9dff..8b2d4904c 100644 --- a/api/candidates.go +++ b/api/candidates.go @@ -1,37 +1,17 @@ package api -import ( - "encoding/json" - "net/http" -) - -func GetCandidates(w http.ResponseWriter, r *http.Request) { - cState, err := GetStateForRequest(r) - +func Candidates(height int) (*[]CandidateResponse, error) { + cState, err := GetStateForHeight(height) if err != nil { - w.Header().Set("Content-Type", "application/json; charset=UTF-8") - w.WriteHeader(http.StatusNotFound) - _ = json.NewEncoder(w).Encode(Response{ - Code: 404, - Log: "State for given height not found", - }) - return + return nil, err } candidates := cState.GetStateCandidates().GetData() - w.Header().Set("Content-Type", "application/json; charset=UTF-8") - - var result []Candidate - + var result []CandidateResponse for _, candidate := range candidates { result = append(result, makeResponseCandidate(candidate, false)) } - w.WriteHeader(http.StatusOK) - - _ = json.NewEncoder(w).Encode(Response{ - Code: 0, - Result: result, - }) + return &result, nil } diff --git a/api/coin_info.go b/api/coin_info.go index 1f84ba674..959c34a10 100644 --- a/api/coin_info.go +++ b/api/coin_info.go @@ -1,10 +1,8 @@ package api import ( - "encoding/json" "github.com/MinterTeam/minter-go-node/core/types" - "github.com/gorilla/mux" - "net/http" + "github.com/pkg/errors" ) type CoinInfoResponse struct { @@ -15,50 +13,22 @@ type CoinInfoResponse struct { ReserveBalance string `json:"reserve_balance"` } -func GetCoinInfo(w http.ResponseWriter, r *http.Request) { - cState, err := GetStateForRequest(r) - +func CoinInfo(coinSymbol string, height int) (*CoinInfoResponse, error) { + cState, err := GetStateForHeight(height) if err != nil { - w.Header().Set("Content-Type", "application/json; charset=UTF-8") - w.WriteHeader(http.StatusNotFound) - _ = json.NewEncoder(w).Encode(Response{ - Code: 404, - Log: "State for given height not found", - }) - return + return nil, err } - vars := mux.Vars(r) - symbol := vars["symbol"] - - var coinSymbol types.CoinSymbol - - copy(coinSymbol[:], []byte(symbol)) - - coin := cState.GetStateCoin(coinSymbol) - - w.Header().Set("Content-Type", "application/json; charset=UTF-8") - + coin := cState.GetStateCoin(types.StrToCoinSymbol(coinSymbol)) if coin == nil { - w.WriteHeader(http.StatusNotFound) - _ = json.NewEncoder(w).Encode(Response{ - Code: 404, - Result: nil, - Log: "Coin not found", - }) - return + return nil, errors.New("Coin not found") } - w.WriteHeader(http.StatusOK) - - _ = json.NewEncoder(w).Encode(Response{ - Code: 0, - Result: CoinInfoResponse{ - Name: coin.Data().Name, - Symbol: coin.Data().Symbol, - Volume: coin.Data().Volume.String(), - Crr: coin.Data().Crr, - ReserveBalance: coin.Data().ReserveBalance.String(), - }, - }) + return &CoinInfoResponse{ + Name: coin.Data().Name, + Symbol: coin.Data().Symbol, + Volume: coin.Data().Volume.String(), + Crr: coin.Data().Crr, + ReserveBalance: coin.Data().ReserveBalance.String(), + }, nil } diff --git a/api/estimate_coin_buy.go b/api/estimate_coin_buy.go index 6c63dcc4e..1d802af4d 100644 --- a/api/estimate_coin_buy.go +++ b/api/estimate_coin_buy.go @@ -1,15 +1,13 @@ package api import ( - "encoding/json" "fmt" - "github.com/MinterTeam/minter-go-node/core/code" "github.com/MinterTeam/minter-go-node/core/commissions" "github.com/MinterTeam/minter-go-node/core/transaction" "github.com/MinterTeam/minter-go-node/core/types" "github.com/MinterTeam/minter-go-node/formula" + "github.com/pkg/errors" "math/big" - "net/http" ) type EstimateCoinBuyResponse struct { @@ -17,109 +15,63 @@ type EstimateCoinBuyResponse struct { Commission string `json:"commission"` } -func EstimateCoinBuy(w http.ResponseWriter, r *http.Request) { - cState, err := GetStateForRequest(r) - +func EstimateCoinBuy(coinToSellString string, coinToBuyString string, valueToBuy *big.Int, height int) (*EstimateCoinBuyResponse, error) { + cState, err := GetStateForHeight(height) if err != nil { - w.Header().Set("Content-Type", "application/json; charset=UTF-8") - w.WriteHeader(http.StatusNotFound) - _ = json.NewEncoder(w).Encode(Response{ - Code: 404, - Log: "State for given height not found", - }) - return + return nil, err } - query := r.URL.Query() - coinToSell := query.Get("coin_to_sell") - coinToBuy := query.Get("coin_to_buy") - valueToBuy, _ := big.NewInt(0).SetString(query.Get("value_to_buy"), 10) - - var coinToSellSymbol types.CoinSymbol - copy(coinToSellSymbol[:], []byte(coinToSell)) - - var coinToBuySymbol types.CoinSymbol - copy(coinToBuySymbol[:], []byte(coinToBuy)) + coinToSell := types.StrToCoinSymbol(coinToSellString) + coinToBuy := types.StrToCoinSymbol(coinToBuyString) var result *big.Int - w.Header().Set("Content-Type", "application/json; charset=UTF-8") - if coinToSell == coinToBuy { - w.WriteHeader(http.StatusBadRequest) - _ = json.NewEncoder(w).Encode(Response{ - Code: code.CrossConvert, - Log: fmt.Sprintf("\"From\" coin equals to \"to\" coin"), - }) - return + return nil, errors.New("\"From\" coin equals to \"to\" coin") } - if !cState.CoinExists(coinToSellSymbol) { - w.WriteHeader(http.StatusBadRequest) - _ = json.NewEncoder(w).Encode(Response{ - Code: code.CrossConvert, - Log: fmt.Sprintf("Coin to sell not exists"), - }) - return + if !cState.CoinExists(coinToSell) { + return nil, errors.New("Coin to sell not exists") } - if !cState.CoinExists(coinToBuySymbol) { - w.WriteHeader(http.StatusBadRequest) - _ = json.NewEncoder(w).Encode(Response{ - Code: code.CrossConvert, - Log: fmt.Sprintf("Coin to buy not exists"), - }) - return + if !cState.CoinExists(coinToBuy) { + return nil, errors.New("Coin to buy not exists") } commissionInBaseCoin := big.NewInt(commissions.ConvertTx) commissionInBaseCoin.Mul(commissionInBaseCoin, transaction.CommissionMultiplier) commission := big.NewInt(0).Set(commissionInBaseCoin) - if coinToSellSymbol != types.GetBaseCoin() { - coin := cState.GetStateCoin(coinToSellSymbol) + if coinToSell != types.GetBaseCoin() { + coin := cState.GetStateCoin(coinToSell) if coin.ReserveBalance().Cmp(commissionInBaseCoin) < 0 { - w.WriteHeader(http.StatusBadRequest) - _ = json.NewEncoder(w).Encode(Response{ - Code: 1, - Log: fmt.Sprintf("Coin reserve balance is not sufficient for transaction. Has: %s, required %s", coin.ReserveBalance().String(), commissionInBaseCoin.String()), - }) - return + return nil, errors.New(fmt.Sprintf("Coin reserve balance is not sufficient for transaction. Has: %s, required %s", coin.ReserveBalance().String(), commissionInBaseCoin.String())) } commission = formula.CalculateSaleAmount(coin.Volume(), coin.ReserveBalance(), coin.Data().Crr, commissionInBaseCoin) } - if coinToSellSymbol == types.GetBaseCoin() { - coin := cState.GetStateCoin(coinToBuySymbol).Data() + if coinToSell == types.GetBaseCoin() { + coin := cState.GetStateCoin(coinToBuy).Data() result = formula.CalculatePurchaseAmount(coin.Volume, coin.ReserveBalance, coin.Crr, valueToBuy) - } else if coinToBuySymbol == types.GetBaseCoin() { - coin := cState.GetStateCoin(coinToSellSymbol).Data() + } else if coinToBuy == types.GetBaseCoin() { + coin := cState.GetStateCoin(coinToSell).Data() result = formula.CalculateSaleAmount(coin.Volume, coin.ReserveBalance, coin.Crr, valueToBuy) } else { - coinFrom := cState.GetStateCoin(coinToSellSymbol).Data() - coinTo := cState.GetStateCoin(coinToBuySymbol).Data() + coinFrom := cState.GetStateCoin(coinToSell).Data() + coinTo := cState.GetStateCoin(coinToBuy).Data() baseCoinNeeded := formula.CalculatePurchaseAmount(coinTo.Volume, coinTo.ReserveBalance, coinTo.Crr, valueToBuy) if coinFrom.ReserveBalance.Cmp(baseCoinNeeded) < 0 { - w.WriteHeader(http.StatusBadRequest) - _ = json.NewEncoder(w).Encode(Response{ - Code: 1, - Log: fmt.Sprintf("Coin reserve balance is not sufficient for transaction. Has: %s, required %s", coinFrom.ReserveBalance.String(), baseCoinNeeded.String()), - }) - return + return nil, errors.New(fmt.Sprintf("Coin reserve balance is not sufficient for transaction. Has: %s, required %s", coinFrom.ReserveBalance.String(), baseCoinNeeded.String())) } result = formula.CalculateSaleAmount(coinFrom.Volume, coinFrom.ReserveBalance, coinFrom.Crr, baseCoinNeeded) } - w.WriteHeader(http.StatusOK) - _ = json.NewEncoder(w).Encode(Response{ - Code: 0, - Result: EstimateCoinBuyResponse{ - WillPay: result.String(), - Commission: commission.String(), - }, - }) + return &EstimateCoinBuyResponse{ + WillPay: result.String(), + Commission: commission.String(), + }, nil } diff --git a/api/estimate_coin_sell.go b/api/estimate_coin_sell.go index 99202fd0c..bd11b3e2d 100644 --- a/api/estimate_coin_sell.go +++ b/api/estimate_coin_sell.go @@ -1,15 +1,13 @@ package api import ( - "encoding/json" "fmt" - "github.com/MinterTeam/minter-go-node/core/code" "github.com/MinterTeam/minter-go-node/core/commissions" "github.com/MinterTeam/minter-go-node/core/transaction" "github.com/MinterTeam/minter-go-node/core/types" "github.com/MinterTeam/minter-go-node/formula" + "github.com/pkg/errors" "math/big" - "net/http" ) type EstimateCoinSellResponse struct { @@ -17,108 +15,62 @@ type EstimateCoinSellResponse struct { Commission string `json:"commission"` } -func EstimateCoinSell(w http.ResponseWriter, r *http.Request) { - cState, err := GetStateForRequest(r) - +func EstimateCoinSell(coinToSellString string, coinToBuyString string, valueToSell *big.Int, height int) (*EstimateCoinSellResponse, error) { + cState, err := GetStateForHeight(height) if err != nil { - w.Header().Set("Content-Type", "application/json; charset=UTF-8") - w.WriteHeader(http.StatusNotFound) - _ = json.NewEncoder(w).Encode(Response{ - Code: 404, - Log: "State for given height not found", - }) - return + return nil, err } - query := r.URL.Query() - coinToSell := query.Get("coin_to_sell") - coinToBuy := query.Get("coin_to_buy") - valueToSell, _ := big.NewInt(0).SetString(query.Get("value_to_sell"), 10) - - var coinToSellSymbol types.CoinSymbol - copy(coinToSellSymbol[:], []byte(coinToSell)) - - var coinToBuySymbol types.CoinSymbol - copy(coinToBuySymbol[:], []byte(coinToBuy)) + coinToSell := types.StrToCoinSymbol(coinToSellString) + coinToBuy := types.StrToCoinSymbol(coinToBuyString) var result *big.Int - w.Header().Set("Content-Type", "application/json; charset=UTF-8") - if coinToSell == coinToBuy { - w.WriteHeader(http.StatusBadRequest) - _ = json.NewEncoder(w).Encode(Response{ - Code: code.CrossConvert, - Log: fmt.Sprintf("\"From\" coin equals to \"to\" coin"), - }) - return + return nil, errors.New("\"From\" coin equals to \"to\" coin") } - if !cState.CoinExists(coinToSellSymbol) { - w.WriteHeader(http.StatusBadRequest) - _ = json.NewEncoder(w).Encode(Response{ - Code: code.CrossConvert, - Log: fmt.Sprintf("Coin to sell not exists"), - }) - return + if !cState.CoinExists(coinToSell) { + return nil, errors.New("Coin to sell not exists") } - if !cState.CoinExists(coinToBuySymbol) { - w.WriteHeader(http.StatusBadRequest) - _ = json.NewEncoder(w).Encode(Response{ - Code: code.CrossConvert, - Log: fmt.Sprintf("Coin to buy not exists"), - }) - return + if !cState.CoinExists(coinToBuy) { + return nil, errors.New("Coin to buy not exists") } commissionInBaseCoin := big.NewInt(commissions.ConvertTx) commissionInBaseCoin.Mul(commissionInBaseCoin, transaction.CommissionMultiplier) commission := big.NewInt(0).Set(commissionInBaseCoin) - if coinToSellSymbol != types.GetBaseCoin() { - coin := cState.GetStateCoin(coinToSellSymbol) + if coinToSell != types.GetBaseCoin() { + coin := cState.GetStateCoin(coinToSell) if coin.ReserveBalance().Cmp(commissionInBaseCoin) < 0 { - w.WriteHeader(http.StatusBadRequest) - _ = json.NewEncoder(w).Encode(Response{ - Code: 1, - Log: fmt.Sprintf("Coin reserve balance is not sufficient for transaction. Has: %s, required %s", coin.ReserveBalance().String(), commissionInBaseCoin.String()), - }) - return + return nil, errors.New(fmt.Sprintf("Coin reserve balance is not sufficient for transaction. Has: %s, required %s", coin.ReserveBalance().String(), commissionInBaseCoin.String())) } if coin.Volume().Cmp(valueToSell) < 0 { - w.WriteHeader(http.StatusBadRequest) - _ = json.NewEncoder(w).Encode(Response{ - Code: 1, - Log: fmt.Sprintf("Coin volume is not sufficient for transaction. Has: %s, required %s", coin.Volume().String(), valueToSell.String()), - }) - return + return nil, errors.New(fmt.Sprintf("Coin volume is not sufficient for transaction. Has: %s, required %s", coin.Volume().String(), valueToSell.String())) } commission = formula.CalculateSaleAmount(coin.Volume(), coin.ReserveBalance(), coin.Data().Crr, commissionInBaseCoin) } - if coinToSellSymbol == types.GetBaseCoin() { - coin := cState.GetStateCoin(coinToBuySymbol).Data() + if coinToSell == types.GetBaseCoin() { + coin := cState.GetStateCoin(coinToBuy).Data() result = formula.CalculatePurchaseReturn(coin.Volume, coin.ReserveBalance, coin.Crr, valueToSell) - } else if coinToBuySymbol == types.GetBaseCoin() { - coin := cState.GetStateCoin(coinToSellSymbol).Data() + } else if coinToBuy == types.GetBaseCoin() { + coin := cState.GetStateCoin(coinToSell).Data() result = formula.CalculateSaleReturn(coin.Volume, coin.ReserveBalance, coin.Crr, valueToSell) } else { - coinFrom := cState.GetStateCoin(coinToSellSymbol).Data() - coinTo := cState.GetStateCoin(coinToBuySymbol).Data() + coinFrom := cState.GetStateCoin(coinToSell).Data() + coinTo := cState.GetStateCoin(coinToBuy).Data() basecoinValue := formula.CalculateSaleReturn(coinFrom.Volume, coinFrom.ReserveBalance, coinFrom.Crr, valueToSell) result = formula.CalculatePurchaseReturn(coinTo.Volume, coinTo.ReserveBalance, coinTo.Crr, basecoinValue) } - w.WriteHeader(http.StatusOK) - _ = json.NewEncoder(w).Encode(Response{ - Code: 0, - Result: EstimateCoinSellResponse{ - WillGet: result.String(), - Commission: commission.String(), - }, - }) + return &EstimateCoinSellResponse{ + WillGet: result.String(), + Commission: commission.String(), + }, nil } diff --git a/api/estimate_tx_commission.go b/api/estimate_tx_commission.go index c1df7325e..ac22cafbf 100644 --- a/api/estimate_tx_commission.go +++ b/api/estimate_tx_commission.go @@ -1,41 +1,22 @@ package api import ( - "encoding/json" "fmt" "github.com/MinterTeam/minter-go-node/core/transaction" "github.com/MinterTeam/minter-go-node/formula" - "github.com/MinterTeam/minter-go-node/hexutil" + "github.com/pkg/errors" "math/big" - "net/http" ) -func EstimateTxCommission(w http.ResponseWriter, r *http.Request) { - cState, err := GetStateForRequest(r) - +func EstimateTxCommission(rawTx []byte, height int) (*big.Int, error) { + cState, err := GetStateForHeight(height) if err != nil { - w.Header().Set("Content-Type", "application/json; charset=UTF-8") - w.WriteHeader(http.StatusNotFound) - _ = json.NewEncoder(w).Encode(Response{ - Code: 404, - Log: "State for given height not found", - }) - return + return nil, err } - query := r.URL.Query() - rawTx := query.Get("tx") - bytesTx, _ := hexutil.Decode("Mx" + rawTx) - - tx, err := transaction.DecodeFromBytes(bytesTx) - + tx, err := transaction.DecodeFromBytes(rawTx) if err != nil { - w.WriteHeader(http.StatusBadRequest) - _ = json.NewEncoder(w).Encode(Response{ - Code: 1, - Log: err.Error(), - }) - return + return nil, err } commissionInBaseCoin := big.NewInt(0).Mul(tx.GasPrice, big.NewInt(tx.Gas())) @@ -46,25 +27,11 @@ func EstimateTxCommission(w http.ResponseWriter, r *http.Request) { coin := cState.GetStateCoin(tx.GasCoin) if coin.ReserveBalance().Cmp(commissionInBaseCoin) < 0 { - - w.WriteHeader(http.StatusBadRequest) - _ = json.NewEncoder(w).Encode(Response{ - Code: 1, - Log: fmt.Sprintf("Coin reserve balance is not sufficient for transaction. Has: %s, required %s", coin.ReserveBalance().String(), commissionInBaseCoin.String()), - }) - return + return nil, errors.New(fmt.Sprintf("Coin reserve balance is not sufficient for transaction. Has: %s, required %s", coin.ReserveBalance().String(), commissionInBaseCoin.String())) } commission = formula.CalculateSaleAmount(coin.Volume(), coin.ReserveBalance(), coin.Data().Crr, commissionInBaseCoin) } - w.WriteHeader(http.StatusOK) - _ = json.NewEncoder(w).Encode(Response{ - Code: 0, - Result: struct { - Commission string `json:"commission"` - }{ - Commission: commission.String(), - }, - }) + return commission, nil } diff --git a/api/net_info.go b/api/net_info.go index 2f589bbeb..8e3467805 100644 --- a/api/net_info.go +++ b/api/net_info.go @@ -1,27 +1,9 @@ package api import ( - "encoding/json" - "net/http" + "github.com/tendermint/tendermint/rpc/core/types" ) -func NetInfo(w http.ResponseWriter, r *http.Request) { - result, err := client.NetInfo() - - w.Header().Set("Content-Type", "application/json; charset=UTF-8") - - if err != nil { - w.WriteHeader(http.StatusOK) - _ = json.NewEncoder(w).Encode(Response{ - Code: 500, - Result: nil, - Log: err.Error(), - }) - return - } - - _ = json.NewEncoder(w).Encode(Response{ - Code: 0, - Result: result, - }) +func NetInfo() (*core_types.ResultNetInfo, error) { + return client.NetInfo() } diff --git a/api/send_transaction.go b/api/send_transaction.go index 63977fa5d..24e83740c 100644 --- a/api/send_transaction.go +++ b/api/send_transaction.go @@ -1,67 +1,9 @@ package api import ( - "encoding/json" - "io" - "io/ioutil" - - "github.com/MinterTeam/minter-go-node/core/code" - "github.com/MinterTeam/minter-go-node/core/types" - "net/http" - "strings" + "github.com/tendermint/tendermint/rpc/core/types" ) -type SendTransactionRequest struct { - Transaction string `json:"transaction"` -} - -type SendTransactionResponse struct { - Hash string `json:"hash"` -} - -func SendTransaction(w http.ResponseWriter, r *http.Request) { - var req SendTransactionRequest - body, _ := ioutil.ReadAll(io.LimitReader(r.Body, 1048576)) - err := json.Unmarshal(body, &req) - - if err != nil { - _ = json.NewEncoder(w).Encode(Response{ - Code: 1, - Log: "Request decode error", - }) - return - } - - result, err := client.BroadcastTxSync(types.Hex2Bytes(req.Transaction)) - - w.Header().Set("Content-Type", "application/json; charset=UTF-8") - - if err != nil { - w.WriteHeader(http.StatusInternalServerError) - - _ = json.NewEncoder(w).Encode(Response{ - Code: 500, - Log: err.Error(), - }) - return - } - - if result.Code != code.OK { - w.WriteHeader(http.StatusInternalServerError) - - _ = json.NewEncoder(w).Encode(Response{ - Code: result.Code, - Log: "Check tx error: " + result.Log, - }) - return - } - - w.WriteHeader(http.StatusOK) - - _ = json.NewEncoder(w).Encode(Response{ - Code: code.OK, - Result: SendTransactionResponse{ - Hash: "Mt" + strings.ToLower(result.Hash.String()), - }, - }) +func SendTransaction(tx []byte) (*core_types.ResultBroadcastTx, error) { + return client.BroadcastTxSync(tx) } diff --git a/api/status.go b/api/status.go index de47509df..cbe3db68e 100644 --- a/api/status.go +++ b/api/status.go @@ -1,11 +1,9 @@ package api import ( - "encoding/json" "fmt" "github.com/MinterTeam/minter-go-node/version" "github.com/tendermint/tendermint/rpc/core/types" - "net/http" "time" ) @@ -18,32 +16,19 @@ type StatusResponse struct { TmStatus *core_types.ResultStatus `json:"tm_status"` } -func Status(w http.ResponseWriter, r *http.Request) { - +func Status() (*StatusResponse, error) { result, err := client.Status() - w.Header().Set("Content-Type", "application/json; charset=UTF-8") - if err != nil { - w.WriteHeader(http.StatusOK) - _ = json.NewEncoder(w).Encode(Response{ - Code: 500, - Log: err.Error(), - }) - return + return nil, err } - tmStatus, err := cdc.MarshalJSON(StatusResponse{ + return &StatusResponse{ MinterVersion: version.Version, LatestBlockHash: fmt.Sprintf("%X", result.SyncInfo.LatestBlockHash), LatestAppHash: fmt.Sprintf("%X", result.SyncInfo.LatestAppHash), LatestBlockHeight: result.SyncInfo.LatestBlockHeight, LatestBlockTime: result.SyncInfo.LatestBlockTime, TmStatus: result, - }) - - _ = json.NewEncoder(w).Encode(Response{ - Code: 0, - Result: json.RawMessage(tmStatus), - }) + }, nil } diff --git a/api/transaction.go b/api/transaction.go index 5c96a9077..b397f9a3e 100644 --- a/api/transaction.go +++ b/api/transaction.go @@ -1,40 +1,21 @@ package api import ( - "encoding/hex" - "encoding/json" "fmt" "github.com/MinterTeam/minter-go-node/core/transaction" - "github.com/gorilla/mux" + "github.com/pkg/errors" "github.com/tendermint/tendermint/libs/common" - "net/http" - "strings" ) -func Transaction(w http.ResponseWriter, r *http.Request) { - - vars := mux.Vars(r) - hash := strings.TrimLeft(vars["hash"], "Mt") - decoded, err := hex.DecodeString(hash) - - tx, err := client.Tx(decoded, false) - - w.Header().Set("Content-Type", "application/json; charset=UTF-8") - - if err != nil || tx.Height > blockchain.LastCommittedHeight() { - w.WriteHeader(http.StatusBadRequest) - err = json.NewEncoder(w).Encode(Response{ - Code: 404, - Result: err.Error(), - }) - - if err != nil { - panic(err) - } - return +func Transaction(hash []byte) (*TransactionResponse, error) { + tx, err := client.Tx(hash, false) + if err != nil { + return nil, err } - w.WriteHeader(http.StatusOK) + if tx.Height > blockchain.LastCommittedHeight() { + return nil, errors.New("Tx not found") + } decodedTx, _ := transaction.DecodeFromBytes(tx.Tx) sender, _ := decodedTx.Sender() @@ -50,28 +31,21 @@ func Transaction(w http.ResponseWriter, r *http.Request) { } } - err = json.NewEncoder(w).Encode(Response{ - Code: 0, - Result: TransactionResponse{ - Hash: common.HexBytes(tx.Tx.Hash()), - RawTx: fmt.Sprintf("%x", []byte(tx.Tx)), - Height: tx.Height, - Index: tx.Index, - From: sender.String(), - Nonce: decodedTx.Nonce, - GasPrice: decodedTx.GasPrice, - GasCoin: decodedTx.GasCoin, - GasUsed: tx.TxResult.GasUsed, - Type: decodedTx.Type, - Data: decodedTx.GetDecodedData(), - Payload: decodedTx.Payload, - Tags: tags, - Code: tx.TxResult.Code, - Log: tx.TxResult.Log, - }, - }) - - if err != nil { - panic(err) - } + return &TransactionResponse{ + Hash: common.HexBytes(tx.Tx.Hash()), + RawTx: fmt.Sprintf("%x", []byte(tx.Tx)), + Height: tx.Height, + Index: tx.Index, + From: sender.String(), + Nonce: decodedTx.Nonce, + GasPrice: decodedTx.GasPrice, + GasCoin: decodedTx.GasCoin, + GasUsed: tx.TxResult.GasUsed, + Type: decodedTx.Type, + Data: decodedTx.GetDecodedData(), + Payload: decodedTx.Payload, + Tags: tags, + Code: tx.TxResult.Code, + Log: tx.TxResult.Log, + }, nil } diff --git a/api/transaction_count.go b/api/transaction_count.go deleted file mode 100644 index 9aa3cbb3b..000000000 --- a/api/transaction_count.go +++ /dev/null @@ -1,39 +0,0 @@ -package api - -import ( - "encoding/json" - "github.com/MinterTeam/minter-go-node/core/types" - "github.com/gorilla/mux" - "net/http" -) - -type TransactionCountResponse struct { - Count uint64 `json:"count"` -} - -func GetTransactionCount(w http.ResponseWriter, r *http.Request) { - cState, err := GetStateForRequest(r) - - if err != nil { - w.Header().Set("Content-Type", "application/json; charset=UTF-8") - w.WriteHeader(http.StatusNotFound) - _ = json.NewEncoder(w).Encode(Response{ - Code: 404, - Log: "State for given height not found", - }) - return - } - - vars := mux.Vars(r) - address := types.HexToAddress(vars["address"]) - - w.Header().Set("Content-Type", "application/json; charset=UTF-8") - w.WriteHeader(http.StatusOK) - - _ = json.NewEncoder(w).Encode(Response{ - Code: 0, - Result: TransactionCountResponse{ - Count: cState.GetNonce(address), - }, - }) -} diff --git a/api/transactions.go b/api/transactions.go index 150164013..c092e72c4 100644 --- a/api/transactions.go +++ b/api/transactions.go @@ -1,14 +1,12 @@ package api import ( - "encoding/json" "fmt" "github.com/MinterTeam/minter-go-node/core/transaction" "github.com/MinterTeam/minter-go-node/core/types" "github.com/tendermint/tendermint/libs/common" "github.com/tendermint/tendermint/rpc/core/types" "math/big" - "net/http" ) type TransactionResponse struct { @@ -34,26 +32,13 @@ type ResultTxSearch struct { TotalCount int `json:"total_count"` } -func Transactions(w http.ResponseWriter, r *http.Request) { - query := r.URL.Query().Get("query") - +func Transactions(query string) (*[]TransactionResponse, error) { rpcResult, err := client.TxSearch(query, false, 1, 100) - - w.Header().Set("Content-Type", "application/json; charset=UTF-8") - if err != nil { - w.WriteHeader(http.StatusBadRequest) - _ = json.NewEncoder(w).Encode(Response{ - Code: 0, - Result: err.Error(), - }) - return + return nil, err } - w.WriteHeader(http.StatusOK) - result := make([]TransactionResponse, len(rpcResult.Txs)) - for i, tx := range rpcResult.Txs { decodedTx, _ := transaction.DecodeFromBytes(tx.Tx) sender, _ := decodedTx.Sender() @@ -88,12 +73,5 @@ func Transactions(w http.ResponseWriter, r *http.Request) { } } - err = json.NewEncoder(w).Encode(Response{ - Code: 0, - Result: result, - }) - - if err != nil { - panic(err) - } + return &result, nil } diff --git a/api/unconfirmed_txs.go b/api/unconfirmed_txs.go new file mode 100644 index 000000000..f1298e470 --- /dev/null +++ b/api/unconfirmed_txs.go @@ -0,0 +1,10 @@ +package api + +import ( + "github.com/pkg/errors" + "github.com/tendermint/tendermint/rpc/core/types" +) + +func UnconfirmedTxs(limit int) (*core_types.ResultUnconfirmedTxs, error) { + return nil, errors.New("not implemented") +} diff --git a/api/validators.go b/api/validators.go index 41e522428..598e61afd 100644 --- a/api/validators.go +++ b/api/validators.go @@ -1,12 +1,10 @@ package api import ( - "encoding/json" "fmt" "github.com/MinterTeam/minter-go-node/core/state" "github.com/MinterTeam/minter-go-node/core/types" - "net/http" - "strconv" + "github.com/pkg/errors" ) type Stake struct { @@ -16,7 +14,7 @@ type Stake struct { BipValue string `json:"bip_value"` } -type Candidate struct { +type CandidateResponse struct { CandidateAddress types.Address `json:"candidate_address"` TotalStake string `json:"total_stake"` PubKey string `json:"pub_key"` @@ -26,22 +24,22 @@ type Candidate struct { Status byte `json:"status"` } -type Validator struct { - AccumReward string `json:"accumulated_reward"` - AbsentTimes int `json:"absent_times"` - Candidate Candidate `json:"candidate"` +type ValidatorResponse struct { + AccumReward string `json:"accumulated_reward"` + AbsentTimes int `json:"absent_times"` + Candidate CandidateResponse `json:"candidate"` } -func makeResponseValidator(v state.Validator, state *state.StateDB) Validator { - return Validator{ +func makeResponseValidator(v state.Validator, state *state.StateDB) ValidatorResponse { + return ValidatorResponse{ AccumReward: v.AccumReward.String(), AbsentTimes: v.CountAbsentTimes(), Candidate: makeResponseCandidate(*state.GetStateCandidate(v.PubKey), false), } } -func makeResponseCandidate(c state.Candidate, includeStakes bool) Candidate { - candidate := Candidate{ +func makeResponseCandidate(c state.Candidate, includeStakes bool) CandidateResponse { + candidate := CandidateResponse{ CandidateAddress: c.CandidateAddress, TotalStake: c.TotalBipStake.String(), PubKey: fmt.Sprintf("Mp%x", c.PubKey), @@ -65,47 +63,24 @@ func makeResponseCandidate(c state.Candidate, includeStakes bool) Candidate { return candidate } -func GetValidators(w http.ResponseWriter, r *http.Request) { - height, _ := strconv.Atoi(r.URL.Query().Get("height")) +type ResponseValidators []ValidatorResponse - if height <= 0 { - height = int(blockchain.Height()) - } - - rState, err := GetStateForRequest(r) +func Validators(height int) (*ResponseValidators, error) { + rState, err := GetStateForHeight(height) if err != nil { - w.Header().Set("Content-Type", "application/json; charset=UTF-8") - w.WriteHeader(http.StatusNotFound) - _ = json.NewEncoder(w).Encode(Response{ - Code: 404, - Log: "State for given height not found", - }) - return + return nil, err } - vals := rState.GetStateValidators() - - w.Header().Set("Content-Type", "application/json; charset=UTF-8") + vals := rState.GetStateValidators() if vals == nil { - w.WriteHeader(http.StatusNotFound) - _ = json.NewEncoder(w).Encode(Response{ - Code: 404, - Log: "Validators not found", - }) - return + return nil, errors.New("Validator not found") } - w.WriteHeader(http.StatusOK) - - var responseValidators []Validator - + var responseValidators ResponseValidators for _, val := range vals.Data() { responseValidators = append(responseValidators, makeResponseValidator(val, rState)) } - _ = json.NewEncoder(w).Encode(Response{ - Code: 0, - Result: responseValidators, - }) + return &responseValidators, nil } diff --git a/config/config.go b/config/config.go index 27ae2a062..1df5d74cc 100644 --- a/config/config.go +++ b/config/config.go @@ -258,8 +258,6 @@ func DefaultBaseConfig() BaseConfig { PrivValidator: defaultPrivValPath, NodeKey: defaultNodeKeyPath, Moniker: defaultMoniker, - ProxyApp: "tcp://127.0.0.1:26658", - ABCI: "socket", LogLevel: DefaultPackageLogLevels(), ProfListenAddress: "", FastSync: true, @@ -267,12 +265,10 @@ func DefaultBaseConfig() BaseConfig { DBBackend: "leveldb", DBPath: "data", GUIListenAddress: ":3000", - APIListenAddress: ":8841", + APIListenAddress: "tcp://0.0.0.0:8841", ValidatorMode: false, KeepStateHistory: false, APISimultaneousRequests: 100, - APIPerIPLimit: 1000, - APIPerIPLimitWindow: 60 * time.Second, LogPath: "stdout", LogFormat: LogFormatPlain, } diff --git a/config/toml.go b/config/toml.go index 9094c1db6..5ede17b7e 100644 --- a/config/toml.go +++ b/config/toml.go @@ -82,12 +82,6 @@ keep_state_history = {{ .BaseConfig.KeepStateHistory }} # Limit for simultaneous requests to API api_simultaneous_requests = {{ .BaseConfig.APISimultaneousRequests }} -# Limit API requests for client (by IP) -api_per_ip_limit = {{ .BaseConfig.APIPerIPLimit }} - -# How often API requests limits will be cleared -api_per_ip_limit_window = "{{ .BaseConfig.APIPerIPLimitWindow }}" - # If this node is many blocks behind the tip of the chain, FastSync # allows them to catchup quickly by downloading blocks in parallel # and verifying their commits diff --git a/core/types/types.go b/core/types/types.go index c0dc0d57e..2c2cdddc3 100644 --- a/core/types/types.go +++ b/core/types/types.go @@ -163,6 +163,12 @@ func (c CoinSymbol) IsBaseCoin() bool { return c.Compare(GetBaseCoin()) == 0 } +func StrToCoinSymbol(s string) CoinSymbol { + var symbol CoinSymbol + copy(symbol[:], []byte(s)) + return symbol +} + /////////// Address // Address represents the 20 byte address of an Ethereum account. @@ -235,6 +241,15 @@ func (a *Address) UnmarshalText(input []byte) error { return hexutil.UnmarshalFixedText("Address", input, a[:]) } +func (a *Address) Unmarshal(input []byte) error { + copy(a[:], input) + return nil +} + +func (a Address) MarshalJSON() ([]byte, error) { + return []byte(fmt.Sprintf("\"%s\"", a.String())), nil +} + // UnmarshalJSON parses a hash in hex syntax. func (a *Address) UnmarshalJSON(input []byte) error { return hexutil.UnmarshalFixedJSON(addressT, input, a[:]) From 54a83d65357872d7285142a916006285cd1240b6 Mon Sep 17 00:00:00 2001 From: Daniil Lashin Date: Thu, 29 Nov 2018 14:04:09 +0300 Subject: [PATCH 03/49] Refactor API --- api/coin_info.go | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/api/coin_info.go b/api/coin_info.go index 959c34a10..dc7c79875 100644 --- a/api/coin_info.go +++ b/api/coin_info.go @@ -3,14 +3,15 @@ package api import ( "github.com/MinterTeam/minter-go-node/core/types" "github.com/pkg/errors" + "math/big" ) type CoinInfoResponse struct { Name string `json:"name"` Symbol types.CoinSymbol `json:"symbol"` - Volume string `json:"volume"` + Volume *big.Int `json:"volume"` Crr uint `json:"crr"` - ReserveBalance string `json:"reserve_balance"` + ReserveBalance *big.Int `json:"reserve_balance"` } func CoinInfo(coinSymbol string, height int) (*CoinInfoResponse, error) { @@ -24,11 +25,12 @@ func CoinInfo(coinSymbol string, height int) (*CoinInfoResponse, error) { return nil, errors.New("Coin not found") } + coinData := coin.Data() return &CoinInfoResponse{ - Name: coin.Data().Name, - Symbol: coin.Data().Symbol, - Volume: coin.Data().Volume.String(), - Crr: coin.Data().Crr, - ReserveBalance: coin.Data().ReserveBalance.String(), + Name: coinData.Name, + Symbol: coinData.Symbol, + Volume: coinData.Volume, + Crr: coinData.Crr, + ReserveBalance: coinData.ReserveBalance, }, nil } From e7f748c7c658da0a5a978a8d867164d76dcf581a Mon Sep 17 00:00:00 2001 From: Daniil Lashin Date: Thu, 29 Nov 2018 14:07:01 +0300 Subject: [PATCH 04/49] Refactor API --- api/address.go | 11 ++++++----- api/block.go | 4 ++-- api/estimate_coin_buy.go | 8 ++++---- api/estimate_coin_sell.go | 8 ++++---- api/validators.go | 9 +++++---- 5 files changed, 21 insertions(+), 19 deletions(-) diff --git a/api/address.go b/api/address.go index 3f9e01dd4..dafc15ffb 100644 --- a/api/address.go +++ b/api/address.go @@ -2,11 +2,12 @@ package api import ( "github.com/MinterTeam/minter-go-node/core/types" + "math/big" ) type AddressResponse struct { - Balance map[string]string `json:"balance"` - TransactionCount uint64 `json:"transaction_count"` + Balance map[string]*big.Int `json:"balance"` + TransactionCount uint64 `json:"transaction_count"` } func Address(address types.Address, height int) (*AddressResponse, error) { @@ -16,17 +17,17 @@ func Address(address types.Address, height int) (*AddressResponse, error) { } response := AddressResponse{ - Balance: make(map[string]string), + Balance: make(map[string]*big.Int), TransactionCount: cState.GetNonce(address), } balances := cState.GetBalances(address) for k, v := range balances.Data { - response.Balance[k.String()] = v.String() + response.Balance[k.String()] = v } if _, exists := response.Balance[types.GetBaseCoin().String()]; !exists { - response.Balance[types.GetBaseCoin().String()] = "0" + response.Balance[types.GetBaseCoin().String()] = big.NewInt(0) } return &response, nil diff --git a/api/block.go b/api/block.go index 1e02dad26..ba8cae1ba 100644 --- a/api/block.go +++ b/api/block.go @@ -27,7 +27,7 @@ type BlockResponse struct { Transactions []BlockTransactionResponse `json:"transactions"` Events eventsdb.Events `json:"events,omitempty"` Precommits []*tmtypes.Vote `json:"precommits"` - BlockReward string `json:"block_reward"` + BlockReward *big.Int `json:"block_reward"` Size int `json:"size"` } @@ -99,7 +99,7 @@ func Block(height int64) (*BlockResponse, error) { TotalTxs: block.Block.TotalTxs, Transactions: txs, Precommits: block.Block.LastCommit.Precommits, - BlockReward: rewards.GetRewardForBlock(uint64(height)).String(), + BlockReward: rewards.GetRewardForBlock(uint64(height)), Size: len(cdc.MustMarshalBinaryLengthPrefixed(block)), Events: edb.LoadEvents(height), }, nil diff --git a/api/estimate_coin_buy.go b/api/estimate_coin_buy.go index 1d802af4d..f4fb70710 100644 --- a/api/estimate_coin_buy.go +++ b/api/estimate_coin_buy.go @@ -11,8 +11,8 @@ import ( ) type EstimateCoinBuyResponse struct { - WillPay string `json:"will_pay"` - Commission string `json:"commission"` + WillPay *big.Int `json:"will_pay"` + Commission *big.Int `json:"commission"` } func EstimateCoinBuy(coinToSellString string, coinToBuyString string, valueToBuy *big.Int, height int) (*EstimateCoinBuyResponse, error) { @@ -71,7 +71,7 @@ func EstimateCoinBuy(coinToSellString string, coinToBuyString string, valueToBuy } return &EstimateCoinBuyResponse{ - WillPay: result.String(), - Commission: commission.String(), + WillPay: result, + Commission: commission, }, nil } diff --git a/api/estimate_coin_sell.go b/api/estimate_coin_sell.go index bd11b3e2d..cb01a54c2 100644 --- a/api/estimate_coin_sell.go +++ b/api/estimate_coin_sell.go @@ -11,8 +11,8 @@ import ( ) type EstimateCoinSellResponse struct { - WillGet string `json:"will_get"` - Commission string `json:"commission"` + WillGet *big.Int `json:"will_get"` + Commission *big.Int `json:"commission"` } func EstimateCoinSell(coinToSellString string, coinToBuyString string, valueToSell *big.Int, height int) (*EstimateCoinSellResponse, error) { @@ -70,7 +70,7 @@ func EstimateCoinSell(coinToSellString string, coinToBuyString string, valueToSe } return &EstimateCoinSellResponse{ - WillGet: result.String(), - Commission: commission.String(), + WillGet: result, + Commission: commission, }, nil } diff --git a/api/validators.go b/api/validators.go index 598e61afd..9cac97ff7 100644 --- a/api/validators.go +++ b/api/validators.go @@ -5,6 +5,7 @@ import ( "github.com/MinterTeam/minter-go-node/core/state" "github.com/MinterTeam/minter-go-node/core/types" "github.com/pkg/errors" + "math/big" ) type Stake struct { @@ -16,7 +17,7 @@ type Stake struct { type CandidateResponse struct { CandidateAddress types.Address `json:"candidate_address"` - TotalStake string `json:"total_stake"` + TotalStake *big.Int `json:"total_stake"` PubKey string `json:"pub_key"` Commission uint `json:"commission"` Stakes []Stake `json:"stakes,omitempty"` @@ -25,14 +26,14 @@ type CandidateResponse struct { } type ValidatorResponse struct { - AccumReward string `json:"accumulated_reward"` + AccumReward *big.Int `json:"accumulated_reward"` AbsentTimes int `json:"absent_times"` Candidate CandidateResponse `json:"candidate"` } func makeResponseValidator(v state.Validator, state *state.StateDB) ValidatorResponse { return ValidatorResponse{ - AccumReward: v.AccumReward.String(), + AccumReward: v.AccumReward, AbsentTimes: v.CountAbsentTimes(), Candidate: makeResponseCandidate(*state.GetStateCandidate(v.PubKey), false), } @@ -41,7 +42,7 @@ func makeResponseValidator(v state.Validator, state *state.StateDB) ValidatorRes func makeResponseCandidate(c state.Candidate, includeStakes bool) CandidateResponse { candidate := CandidateResponse{ CandidateAddress: c.CandidateAddress, - TotalStake: c.TotalBipStake.String(), + TotalStake: c.TotalBipStake, PubKey: fmt.Sprintf("Mp%x", c.PubKey), Commission: c.Commission, CreatedAtBlock: c.CreatedAtBlock, From 8e1f08b0015fef84a3195ed46fbdf35e2410f530 Mon Sep 17 00:00:00 2001 From: Daniil Lashin Date: Mon, 3 Dec 2018 12:05:27 +0300 Subject: [PATCH 05/49] Refactor BuyCoin tx --- core/state/statedb.go | 16 ++ core/transaction/buy_coin.go | 271 +++++++++++++++----------------- core/transaction/transaction.go | 36 ++++- formula/formula.go | 9 -- 4 files changed, 175 insertions(+), 157 deletions(-) diff --git a/core/state/statedb.go b/core/state/statedb.go index e468206b7..8c57cad02 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -771,6 +771,10 @@ func (s *StateDB) GetStateCoin(symbol types.CoinSymbol) *stateCoin { } func (s *StateDB) AddCoinVolume(symbol types.CoinSymbol, value *big.Int) { + if symbol.IsBaseCoin() { + return + } + stateCoin := s.GetStateCoin(symbol) if stateCoin != nil { stateCoin.AddVolume(value) @@ -778,6 +782,10 @@ func (s *StateDB) AddCoinVolume(symbol types.CoinSymbol, value *big.Int) { } func (s *StateDB) SubCoinVolume(symbol types.CoinSymbol, value *big.Int) { + if symbol.IsBaseCoin() { + return + } + stateCoin := s.GetStateCoin(symbol) if stateCoin != nil { stateCoin.SubVolume(value) @@ -785,6 +793,10 @@ func (s *StateDB) SubCoinVolume(symbol types.CoinSymbol, value *big.Int) { } func (s *StateDB) AddCoinReserve(symbol types.CoinSymbol, value *big.Int) { + if symbol.IsBaseCoin() { + return + } + stateCoin := s.GetStateCoin(symbol) if stateCoin != nil { stateCoin.AddReserve(value) @@ -792,6 +804,10 @@ func (s *StateDB) AddCoinReserve(symbol types.CoinSymbol, value *big.Int) { } func (s *StateDB) SubCoinReserve(symbol types.CoinSymbol, value *big.Int) { + if symbol.IsBaseCoin() { + return + } + stateCoin := s.GetStateCoin(symbol) if stateCoin != nil { stateCoin.SubReserve(value) diff --git a/core/transaction/buy_coin.go b/core/transaction/buy_coin.go index 12614008c..cb8b636e5 100644 --- a/core/transaction/buy_coin.go +++ b/core/transaction/buy_coin.go @@ -2,7 +2,6 @@ package transaction import ( "encoding/hex" - "encoding/json" "fmt" "github.com/MinterTeam/minter-go-node/core/code" "github.com/MinterTeam/minter-go-node/core/commissions" @@ -19,18 +18,6 @@ type BuyCoinData struct { CoinToSell types.CoinSymbol } -func (data BuyCoinData) MarshalJSON() ([]byte, error) { - return json.Marshal(struct { - CoinToBuy types.CoinSymbol `json:"coin_to_buy,string"` - ValueToBuy string `json:"value_to_buy"` - CoinToSell types.CoinSymbol `json:"coin_to_sell,string"` - }{ - CoinToBuy: data.CoinToBuy, - ValueToBuy: data.ValueToBuy.String(), - CoinToSell: data.CoinToSell, - }) -} - func (data BuyCoinData) String() string { return fmt.Sprintf("BUY COIN sell:%s buy:%s %s", data.CoinToSell.String(), data.ValueToBuy.String(), data.CoinToBuy.String()) @@ -40,182 +27,174 @@ func (data BuyCoinData) Gas() int64 { return commissions.ConvertTx } -func (data BuyCoinData) Run(sender types.Address, tx *Transaction, context *state.StateDB, isCheck bool, rewardPool *big.Int, currentBlock int64) Response { - if data.CoinToSell == data.CoinToBuy { - return Response{ - Code: code.CrossConvert, - Log: fmt.Sprintf("\"From\" coin equals to \"to\" coin")} - } - - if !context.CoinExists(tx.GasCoin) { - return Response{ - Code: code.CoinNotExists, - Log: fmt.Sprintf("Coin %s not exists", tx.GasCoin)} - } - - if !context.CoinExists(data.CoinToSell) { - return Response{ - Code: code.CoinNotExists, - Log: fmt.Sprintf("Coin %s not exists", data.CoinToSell)} - } - - if !context.CoinExists(data.CoinToBuy) { - return Response{ - Code: code.CoinNotExists, - Log: fmt.Sprintf("Coin %s not exists", data.CoinToBuy)} - } - +func (data BuyCoinData) CommissionInBaseCoin(tx *Transaction) *big.Int { commissionInBaseCoin := big.NewInt(0).Mul(tx.GasPrice, big.NewInt(tx.Gas())) commissionInBaseCoin.Mul(commissionInBaseCoin, CommissionMultiplier) - commission := big.NewInt(0).Set(commissionInBaseCoin) - - if !tx.GasCoin.IsBaseCoin() { - coin := context.GetStateCoin(tx.GasCoin) - - if coin.ReserveBalance().Cmp(commissionInBaseCoin) < 0 { - return Response{ - Code: code.CoinReserveNotSufficient, - Log: fmt.Sprintf("Gas coin reserve balance is not sufficient for transaction. Has: %s %s, required %s %s", coin.ReserveBalance().String(), types.GetBaseCoin(), commissionInBaseCoin.String(), types.GetBaseCoin())} - } - commission = formula.CalculateSaleAmount(coin.Volume(), coin.ReserveBalance(), coin.Data().Crr, commissionInBaseCoin) + return commissionInBaseCoin +} - if commission == nil { - return Response{ - Code: 999, - Log: "Unknown error"} - } - } +func (data BuyCoinData) TotalSpend(tx *Transaction, context *state.StateDB) (TotalSpends, []Conversion, *big.Int, *Response) { + total := TotalSpends{} + var conversions []Conversion - if context.GetBalance(sender, tx.GasCoin).Cmp(commission) < 0 { - return Response{ - Code: code.InsufficientFunds, - Log: fmt.Sprintf("Insufficient funds for sender account: %s. Wanted %s %s.", sender.String(), commission.String(), tx.GasCoin)} - } + commissionInBaseCoin := data.CommissionInBaseCoin(tx) + commissionIncluded := false var value *big.Int if data.CoinToSell.IsBaseCoin() { coin := context.GetStateCoin(data.CoinToBuy).Data() - value = formula.CalculatePurchaseAmount(coin.Volume, coin.ReserveBalance, coin.Crr, data.ValueToBuy) + total.Add(data.CoinToSell, value) + conversions = append(conversions, Conversion{ + FromCoin: data.CoinToSell, + ToCoin: data.CoinToBuy, + ToAmount: data.ValueToBuy, + ToReserve: value, + }) + } else if data.CoinToBuy.IsBaseCoin() { + valueToBuy := big.NewInt(0).Set(data.ValueToBuy) - if context.GetBalance(sender, data.CoinToSell).Cmp(value) < 0 { - return Response{ - Code: code.InsufficientFunds, - Log: fmt.Sprintf("Insufficient funds for sender account: %s. Wanted %s %s", sender.String(), value.String(), data.CoinToSell)} + if tx.GasCoin == data.CoinToSell { + commissionIncluded = true + valueToBuy.Add(valueToBuy, commissionInBaseCoin) } - if data.CoinToSell == tx.GasCoin { - totalTxCost := big.NewInt(0) - totalTxCost.Add(totalTxCost, value) - totalTxCost.Add(totalTxCost, commission) - - if context.GetBalance(sender, data.CoinToSell).Cmp(totalTxCost) < 0 { - return Response{ - Code: code.InsufficientFunds, - Log: fmt.Sprintf("Insufficient funds for sender account: %s. Wanted %s %s", sender.String(), totalTxCost.String(), tx.GasCoin)} - } - } + coin := context.GetStateCoin(data.CoinToSell).Data() + value = formula.CalculateSaleAmount(coin.Volume, coin.ReserveBalance, coin.Crr, valueToBuy) + total.Add(data.CoinToSell, value) + conversions = append(conversions, Conversion{ + FromCoin: data.CoinToSell, + FromAmount: value, + FromReserve: valueToBuy, + ToCoin: data.CoinToBuy, + }) + } else { + valueToBuy := big.NewInt(0).Set(data.ValueToBuy) - if !isCheck { - context.SubBalance(sender, data.CoinToSell, value) - context.AddCoinVolume(data.CoinToBuy, data.ValueToBuy) - context.AddCoinReserve(data.CoinToBuy, value) + if tx.GasCoin == data.CoinToSell { + commissionIncluded = true + valueToBuy.Add(valueToBuy, commissionInBaseCoin) } - } else if data.CoinToBuy.IsBaseCoin() { - coin := context.GetStateCoin(data.CoinToSell).Data() - value = formula.CalculateSaleAmount(coin.Volume, coin.ReserveBalance, coin.Crr, data.ValueToBuy) + coinFrom := context.GetStateCoin(data.CoinToSell).Data() + coinTo := context.GetStateCoin(data.CoinToBuy).Data() + baseCoinNeeded := formula.CalculatePurchaseAmount(coinTo.Volume, coinTo.ReserveBalance, coinTo.Crr, valueToBuy) - if value == nil { - return Response{ - Code: 999, - Log: "Unknown error"} + if coinFrom.ReserveBalance.Cmp(baseCoinNeeded) < 0 { + return nil, nil, nil, &Response{ + Code: code.CoinReserveNotSufficient, + Log: fmt.Sprintf("Gas coin reserve balance is not sufficient for transaction. Has: %s %s, required %s %s", + coinFrom.ReserveBalance.String(), + types.GetBaseCoin(), + baseCoinNeeded.String(), + types.GetBaseCoin())} } - if context.GetBalance(sender, data.CoinToSell).Cmp(value) < 0 { - return Response{ - Code: code.InsufficientFunds, - Log: fmt.Sprintf("Insufficient funds for sender account: %s. Wanted %s %s", sender.String(), value.String(), data.CoinToSell)} - } + value = formula.CalculateSaleAmount(coinFrom.Volume, coinFrom.ReserveBalance, coinFrom.Crr, baseCoinNeeded) + total.Add(data.CoinToSell, value) + conversions = append(conversions, Conversion{ + FromCoin: data.CoinToSell, + FromAmount: value, + FromReserve: baseCoinNeeded, + ToCoin: data.CoinToBuy, + ToAmount: valueToBuy, + ToReserve: baseCoinNeeded, + }) + } - if data.CoinToSell == tx.GasCoin { - totalTxCost := big.NewInt(0) - totalTxCost.Add(totalTxCost, value) - totalTxCost.Add(totalTxCost, commission) + if !commissionIncluded { + commission := big.NewInt(0).Set(commissionInBaseCoin) - if context.GetBalance(sender, data.CoinToSell).Cmp(totalTxCost) < 0 { - return Response{ - Code: code.InsufficientFunds, - Log: fmt.Sprintf("Insufficient funds for sender account: %s. Wanted %s %s", sender.String(), totalTxCost.String(), tx.GasCoin)} + if !tx.GasCoin.IsBaseCoin() { + coin := context.GetStateCoin(tx.GasCoin) + + if coin.ReserveBalance().Cmp(commissionInBaseCoin) < 0 { + return nil, nil, nil, &Response{ + Code: code.CoinReserveNotSufficient, + Log: fmt.Sprintf("Gas coin reserve balance is not sufficient for transaction. Has: %s %s, required %s %s", + coin.ReserveBalance().String(), + types.GetBaseCoin(), + commissionInBaseCoin.String(), + types.GetBaseCoin())} } - } - if !isCheck { - context.SubBalance(sender, data.CoinToSell, value) - context.SubCoinVolume(data.CoinToSell, value) - context.SubCoinReserve(data.CoinToSell, data.ValueToBuy) + commission = formula.CalculateSaleAmount(coin.Volume(), coin.ReserveBalance(), coin.Data().Crr, commissionInBaseCoin) } - } else { - coinFrom := context.GetStateCoin(data.CoinToSell).Data() - coinTo := context.GetStateCoin(data.CoinToBuy).Data() - baseCoinNeeded := formula.CalculatePurchaseAmount(coinTo.Volume, coinTo.ReserveBalance, coinTo.Crr, data.ValueToBuy) + total.Add(tx.GasCoin, commission) + } - if coinFrom.ReserveBalance.Cmp(baseCoinNeeded) < 0 { - return Response{ - Code: code.CoinReserveNotSufficient, - Log: fmt.Sprintf("Gas coin reserve balance is not sufficient for transaction. Has: %s %s, required %s %s", coinFrom.ReserveBalance.String(), types.GetBaseCoin(), baseCoinNeeded.String(), types.GetBaseCoin())} - } + return total, conversions, value, nil +} - value = formula.CalculateSaleAmount(coinFrom.Volume, coinFrom.ReserveBalance, coinFrom.Crr, baseCoinNeeded) +func (data BuyCoinData) BasicCheck(tx *Transaction, context *state.StateDB) *Response { + if data.CoinToSell == data.CoinToBuy { + return &Response{ + Code: code.CrossConvert, + Log: fmt.Sprintf("\"From\" coin equals to \"to\" coin")} + } - if value == nil { - return Response{ - Code: 999, - Log: "Unknown error"} - } + if !context.CoinExists(tx.GasCoin) { + return &Response{ + Code: code.CoinNotExists, + Log: fmt.Sprintf("Coin %s not exists", tx.GasCoin)} + } - if context.GetBalance(sender, data.CoinToSell).Cmp(value) < 0 { - return Response{ - Code: code.InsufficientFunds, - Log: fmt.Sprintf("Insufficient funds for sender account: %s. Wanted %s %s", sender.String(), value.String(), data.CoinToSell)} - } + if !context.CoinExists(data.CoinToSell) { + return &Response{ + Code: code.CoinNotExists, + Log: fmt.Sprintf("Coin %s not exists", data.CoinToSell)} + } + + if !context.CoinExists(data.CoinToBuy) { + return &Response{ + Code: code.CoinNotExists, + Log: fmt.Sprintf("Coin %s not exists", data.CoinToBuy)} + } - if data.CoinToSell == tx.GasCoin { - totalTxCost := big.NewInt(0) - totalTxCost.Add(totalTxCost, value) - totalTxCost.Add(totalTxCost, commission) + return nil +} - if context.GetBalance(sender, data.CoinToSell).Cmp(totalTxCost) < 0 { - return Response{ - Code: code.InsufficientFunds, - Log: fmt.Sprintf("Insufficient funds for sender account: %s. Wanted %s %s", sender.String(), totalTxCost.String(), tx.GasCoin)} - } - } +func (data BuyCoinData) Run(tx *Transaction, context *state.StateDB, isCheck bool, rewardPool *big.Int, currentBlock int64) Response { + sender, _ := tx.Sender() - if !isCheck { - context.SubBalance(sender, data.CoinToSell, value) + response := data.BasicCheck(tx, context) + if response != nil { + return *response + } - context.AddCoinVolume(data.CoinToBuy, data.ValueToBuy) - context.SubCoinVolume(data.CoinToSell, value) + totalSpends, conversions, value, response := data.TotalSpend(tx, context) + if response != nil { + return *response + } - context.AddCoinReserve(data.CoinToBuy, baseCoinNeeded) - context.SubCoinReserve(data.CoinToSell, baseCoinNeeded) + for _, ts := range totalSpends { + if context.GetBalance(sender, ts.Coin).Cmp(ts.Value) < 0 { + return Response{ + Code: code.InsufficientFunds, + Log: fmt.Sprintf("Insufficient funds for sender account: %s. Wanted %s %s.", + sender.String(), + ts.Value.String(), + ts.Coin)} } } if !isCheck { - rewardPool.Add(rewardPool, commissionInBaseCoin) + for _, ts := range totalSpends { + context.SubBalance(sender, ts.Coin, ts.Value) + } - context.SubBalance(sender, tx.GasCoin, commission) + for _, conversion := range conversions { + context.SubCoinVolume(conversion.FromCoin, conversion.FromAmount) + context.SubCoinReserve(conversion.FromCoin, conversion.FromReserve) - if !tx.GasCoin.IsBaseCoin() { - context.SubCoinVolume(tx.GasCoin, commission) - context.SubCoinReserve(tx.GasCoin, commissionInBaseCoin) + context.AddCoinVolume(conversion.ToCoin, conversion.ToAmount) + context.AddCoinReserve(conversion.ToCoin, conversion.ToReserve) } + rewardPool.Add(rewardPool, data.CommissionInBaseCoin(tx)) context.AddBalance(sender, data.CoinToBuy, data.ValueToBuy) context.SetNonce(sender, tx.Nonce) } diff --git a/core/transaction/transaction.go b/core/transaction/transaction.go index b6dbbfb06..f046cef71 100644 --- a/core/transaction/transaction.go +++ b/core/transaction/transaction.go @@ -66,11 +66,43 @@ type SignatureMulti struct { type RawData []byte +type TotalSpends []TotalSpend + +func (tss *TotalSpends) Add(coin types.CoinSymbol, value *big.Int) { + for i, t := range *tss { + if t.Coin == coin { + (*tss)[i].Value.Add((*tss)[i].Value, value) + return + } + } + + *tss = append(*tss, TotalSpend{ + Coin: coin, + Value: value, + }) +} + +type TotalSpend struct { + Coin types.CoinSymbol + Value *big.Int +} + +type Conversion struct { + FromCoin types.CoinSymbol + FromAmount *big.Int + FromReserve *big.Int + ToCoin types.CoinSymbol + ToAmount *big.Int + ToReserve *big.Int +} + type Data interface { - MarshalJSON() ([]byte, error) String() string Gas() int64 - Run(sender types.Address, tx *Transaction, context *state.StateDB, isCheck bool, rewardPool *big.Int, currentBlock int64) Response + TotalSpend(tx *Transaction, context *state.StateDB) (TotalSpends, []Conversion, *big.Int, *Response) + CommissionInBaseCoin(tx *Transaction) *big.Int + BasicCheck(tx *Transaction, context *state.StateDB) *Response + Run(tx *Transaction, context *state.StateDB, isCheck bool, rewardPool *big.Int, currentBlock int64) Response } func (tx *Transaction) Serialize() ([]byte, error) { diff --git a/formula/formula.go b/formula/formula.go index 868d57263..404ea0251 100644 --- a/formula/formula.go +++ b/formula/formula.go @@ -38,11 +38,6 @@ func CalculatePurchaseReturn(supply *big.Int, reserve *big.Int, crr uint, deposi result, _ := res.Int(nil) - // TODO: delete - if result == nil { - return big.NewInt(0) - } - return result } @@ -122,10 +117,6 @@ func CalculateSaleAmount(supply *big.Int, reserve *big.Int, crr uint, wantReceiv return ret } - if reserve.Cmp(wantReceive) == -1 { - return nil - } - tSupply := newFloat(0).SetInt(supply) tReserve := newFloat(0).SetInt(reserve) tWantReceive := newFloat(0).SetInt(wantReceive) From d2a42fb56c9c66468626275ca920835f4a78a5fb Mon Sep 17 00:00:00 2001 From: Daniil Lashin Date: Mon, 3 Dec 2018 12:13:04 +0300 Subject: [PATCH 06/49] Refactor Send tx --- core/transaction/send.go | 129 +++++++++++++++++++++------------------ 1 file changed, 70 insertions(+), 59 deletions(-) diff --git a/core/transaction/send.go b/core/transaction/send.go index 0fe91d14a..cce9dac25 100644 --- a/core/transaction/send.go +++ b/core/transaction/send.go @@ -2,7 +2,6 @@ package transaction import ( "encoding/hex" - "encoding/json" "fmt" "github.com/MinterTeam/minter-go-node/core/code" "github.com/MinterTeam/minter-go-node/core/commissions" @@ -19,96 +18,108 @@ type SendData struct { Value *big.Int } -func (data SendData) MarshalJSON() ([]byte, error) { - return json.Marshal(struct { - Coin types.CoinSymbol `json:"coin,string"` - To types.Address `json:"to"` - Value string `json:"value"` - }{ - Coin: data.Coin, - To: data.To, - Value: data.Value.String(), - }) -} +func (data SendData) TotalSpend(tx *Transaction, context *state.StateDB) (TotalSpends, []Conversion, *big.Int, *Response) { + total := TotalSpends{} + var conversions []Conversion -func (data SendData) String() string { - return fmt.Sprintf("SEND to:%s coin:%s value:%s", - data.To.String(), data.Coin.String(), data.Value.String()) + commissionInBaseCoin := data.CommissionInBaseCoin(tx) + commission := big.NewInt(0).Set(commissionInBaseCoin) + + if !tx.GasCoin.IsBaseCoin() { + coin := context.GetStateCoin(tx.GasCoin) + + if coin.ReserveBalance().Cmp(commissionInBaseCoin) < 0 { + return nil, nil, nil, &Response{ + Code: code.CoinReserveNotSufficient, + Log: fmt.Sprintf("Coin reserve balance is not sufficient for transaction. Has: %s, required %s", + coin.ReserveBalance().String(), + commissionInBaseCoin.String())} + } + + commission = formula.CalculateSaleAmount(coin.Volume(), coin.ReserveBalance(), coin.Data().Crr, commissionInBaseCoin) + conversions = append(conversions, Conversion{ + FromCoin: tx.GasCoin, + FromAmount: commission, + FromReserve: commissionInBaseCoin, + ToCoin: types.GetBaseCoin(), + }) + } + + total.Add(tx.GasCoin, commission) + + return total, conversions, nil, nil } -func (data SendData) Gas() int64 { - return commissions.SendTx +func (data SendData) CommissionInBaseCoin(tx *Transaction) *big.Int { + commissionInBaseCoin := big.NewInt(0).Mul(tx.GasPrice, big.NewInt(tx.Gas())) + commissionInBaseCoin.Mul(commissionInBaseCoin, CommissionMultiplier) + + return commissionInBaseCoin } -func (data SendData) Run(sender types.Address, tx *Transaction, context *state.StateDB, isCheck bool, rewardPool *big.Int, currentBlock int64) Response { +func (data SendData) BasicCheck(tx *Transaction, context *state.StateDB) *Response { if !context.CoinExists(data.Coin) { - return Response{ + return &Response{ Code: code.CoinNotExists, Log: fmt.Sprintf("Coin %s not exists", data.Coin)} } if !context.CoinExists(tx.GasCoin) { - return Response{ + return &Response{ Code: code.CoinNotExists, Log: fmt.Sprintf("Coin %s not exists", tx.GasCoin)} } - commissionInBaseCoin := big.NewInt(0).Mul(tx.GasPrice, big.NewInt(tx.Gas())) - commissionInBaseCoin.Mul(commissionInBaseCoin, CommissionMultiplier) - commission := big.NewInt(0).Set(commissionInBaseCoin) - - if !tx.GasCoin.IsBaseCoin() { - coin := context.GetStateCoin(tx.GasCoin) + return nil +} - if coin.ReserveBalance().Cmp(commissionInBaseCoin) < 0 { - return Response{ - Code: code.CoinReserveNotSufficient, - Log: fmt.Sprintf("Coin reserve balance is not sufficient for transaction. Has: %s, required %s", coin.ReserveBalance().String(), commissionInBaseCoin.String())} - } +func (data SendData) String() string { + return fmt.Sprintf("SEND to:%s coin:%s value:%s", + data.To.String(), data.Coin.String(), data.Value.String()) +} - commission = formula.CalculateSaleAmount(coin.Volume(), coin.ReserveBalance(), coin.Data().Crr, commissionInBaseCoin) +func (data SendData) Gas() int64 { + return commissions.SendTx +} - if commission == nil { - return Response{ - Code: 999, - Log: "Unknown error"} - } - } +func (data SendData) Run(tx *Transaction, context *state.StateDB, isCheck bool, rewardPool *big.Int, currentBlock int64) Response { + sender, _ := tx.Sender() - if context.GetBalance(sender, data.Coin).Cmp(data.Value) < 0 { - return Response{ - Code: code.InsufficientFunds, - Log: fmt.Sprintf("Insufficient funds for sender account: %s. Wanted %s %s", sender.String(), data.Value, data.Coin)} + response := data.BasicCheck(tx, context) + if response != nil { + return *response } - if context.GetBalance(sender, tx.GasCoin).Cmp(commission) < 0 { - return Response{ - Code: code.InsufficientFunds, - Log: fmt.Sprintf("Insufficient funds for sender account: %s. Wanted %s %s", sender.String(), commission, tx.GasCoin)} + totalSpends, conversions, _, response := data.TotalSpend(tx, context) + if response != nil { + return *response } - if data.Coin == tx.GasCoin { - totalTxCost := big.NewInt(0) - totalTxCost.Add(totalTxCost, data.Value) - totalTxCost.Add(totalTxCost, commission) - - if context.GetBalance(sender, tx.GasCoin).Cmp(totalTxCost) < 0 { + for _, ts := range totalSpends { + if context.GetBalance(sender, ts.Coin).Cmp(ts.Value) < 0 { return Response{ Code: code.InsufficientFunds, - Log: fmt.Sprintf("Insufficient funds for sender account: %s. Wanted %s %s", sender.String(), totalTxCost.String(), tx.GasCoin)} + Log: fmt.Sprintf("Insufficient funds for sender account: %s. Wanted %s %s.", + sender.String(), + ts.Value.String(), + ts.Coin)} } } if !isCheck { - rewardPool.Add(rewardPool, commissionInBaseCoin) + for _, ts := range totalSpends { + context.SubBalance(sender, ts.Coin, ts.Value) + } + + for _, conversion := range conversions { + context.SubCoinVolume(conversion.FromCoin, conversion.FromAmount) + context.SubCoinReserve(conversion.FromCoin, conversion.FromReserve) - if !tx.GasCoin.IsBaseCoin() { - context.SubCoinVolume(tx.GasCoin, commission) - context.SubCoinReserve(tx.GasCoin, commissionInBaseCoin) + context.AddCoinVolume(conversion.ToCoin, conversion.ToAmount) + context.AddCoinReserve(conversion.ToCoin, conversion.ToReserve) } - context.SubBalance(sender, tx.GasCoin, commission) - context.SubBalance(sender, data.Coin, data.Value) + rewardPool.Add(rewardPool, data.CommissionInBaseCoin(tx)) context.AddBalance(data.To, data.Coin, data.Value) context.SetNonce(sender, tx.Nonce) } From a98eeb65a88f3d82a581ed680ee203dac0d42446 Mon Sep 17 00:00:00 2001 From: Daniil Lashin Date: Mon, 3 Dec 2018 12:56:40 +0300 Subject: [PATCH 07/49] Refactor SellCoin tx --- core/transaction/buy_coin.go | 11 +- core/transaction/redeem_check.go | 30 ++--- core/transaction/sell_coin.go | 222 +++++++++++++++++++++---------- core/transaction/send.go | 11 +- core/transaction/transaction.go | 8 +- 5 files changed, 175 insertions(+), 107 deletions(-) diff --git a/core/transaction/buy_coin.go b/core/transaction/buy_coin.go index cb8b636e5..9b24914d6 100644 --- a/core/transaction/buy_coin.go +++ b/core/transaction/buy_coin.go @@ -27,18 +27,11 @@ func (data BuyCoinData) Gas() int64 { return commissions.ConvertTx } -func (data BuyCoinData) CommissionInBaseCoin(tx *Transaction) *big.Int { - commissionInBaseCoin := big.NewInt(0).Mul(tx.GasPrice, big.NewInt(tx.Gas())) - commissionInBaseCoin.Mul(commissionInBaseCoin, CommissionMultiplier) - - return commissionInBaseCoin -} - func (data BuyCoinData) TotalSpend(tx *Transaction, context *state.StateDB) (TotalSpends, []Conversion, *big.Int, *Response) { total := TotalSpends{} var conversions []Conversion - commissionInBaseCoin := data.CommissionInBaseCoin(tx) + commissionInBaseCoin := tx.CommissionInBaseCoin() commissionIncluded := false var value *big.Int @@ -194,7 +187,7 @@ func (data BuyCoinData) Run(tx *Transaction, context *state.StateDB, isCheck boo context.AddCoinReserve(conversion.ToCoin, conversion.ToReserve) } - rewardPool.Add(rewardPool, data.CommissionInBaseCoin(tx)) + rewardPool.Add(rewardPool, tx.CommissionInBaseCoin()) context.AddBalance(sender, data.CoinToBuy, data.ValueToBuy) context.SetNonce(sender, tx.Nonce) } diff --git a/core/transaction/redeem_check.go b/core/transaction/redeem_check.go index 70163fb3d..47e5bc355 100644 --- a/core/transaction/redeem_check.go +++ b/core/transaction/redeem_check.go @@ -3,7 +3,6 @@ package transaction import ( "bytes" "encoding/hex" - "encoding/json" "fmt" "github.com/MinterTeam/minter-go-node/core/check" "github.com/MinterTeam/minter-go-node/core/code" @@ -23,14 +22,16 @@ type RedeemCheckData struct { Proof [65]byte } -func (data RedeemCheckData) MarshalJSON() ([]byte, error) { - return json.Marshal(struct { - RawCheck string `json:"raw_check"` - Proof string `json:"proof"` - }{ - RawCheck: fmt.Sprintf("Mc%x", data.RawCheck), - Proof: fmt.Sprintf("%x", data.Proof), - }) +func (data RedeemCheckData) TotalSpend(tx *Transaction, context *state.StateDB) (TotalSpends, []Conversion, *big.Int, *Response) { + panic("implement me") +} + +func (data RedeemCheckData) CommissionInBaseCoin(tx *Transaction) *big.Int { + panic("implement me") +} + +func (data RedeemCheckData) BasicCheck(tx *Transaction, context *state.StateDB) *Response { + panic("implement me") } func (data RedeemCheckData) String() string { @@ -41,7 +42,8 @@ func (data RedeemCheckData) Gas() int64 { return commissions.SendTx } -func (data RedeemCheckData) Run(sender types.Address, tx *Transaction, context *state.StateDB, isCheck bool, rewardPool *big.Int, currentBlock int64) Response { +func (data RedeemCheckData) Run(tx *Transaction, context *state.StateDB, isCheck bool, rewardPool *big.Int, currentBlock int64) Response { + sender, _ := tx.Sender() decodedCheck, err := check.DecodeFromBytes(data.RawCheck) if err != nil { @@ -99,7 +101,7 @@ func (data RedeemCheckData) Run(sender types.Address, tx *Transaction, context * var senderAddressHash types.Hash hw := sha3.NewKeccak256() - rlp.Encode(hw, []interface{}{ + _ = rlp.Encode(hw, []interface{}{ sender, }) hw.Sum(senderAddressHash[:0]) @@ -125,12 +127,6 @@ func (data RedeemCheckData) Run(sender types.Address, tx *Transaction, context * if !decodedCheck.Coin.IsBaseCoin() { coin := context.GetStateCoin(decodedCheck.Coin) commission = formula.CalculateSaleAmount(coin.Volume(), coin.ReserveBalance(), coin.Data().Crr, commissionInBaseCoin) - - if commission == nil { - return Response{ - Code: 999, - Log: "Unknown error"} - } } totalTxCost := big.NewInt(0).Add(decodedCheck.Value, commission) diff --git a/core/transaction/sell_coin.go b/core/transaction/sell_coin.go index a22fda3d3..2fb38fe2c 100644 --- a/core/transaction/sell_coin.go +++ b/core/transaction/sell_coin.go @@ -2,7 +2,6 @@ package transaction import ( "encoding/hex" - "encoding/json" "fmt" "github.com/MinterTeam/minter-go-node/core/code" "github.com/MinterTeam/minter-go-node/core/commissions" @@ -19,109 +18,167 @@ type SellCoinData struct { CoinToBuy types.CoinSymbol } -func (data SellCoinData) MarshalJSON() ([]byte, error) { - return json.Marshal(struct { - CoinToSell types.CoinSymbol `json:"coin_to_sell,string"` - ValueToSell string `json:"value_to_sell"` - CoinToBuy types.CoinSymbol `json:"coin_to_buy,string"` - }{ - CoinToSell: data.CoinToSell, - ValueToSell: data.ValueToSell.String(), - CoinToBuy: data.CoinToBuy, - }) -} +func (data SellCoinData) TotalSpend(tx *Transaction, context *state.StateDB) (TotalSpends, []Conversion, *big.Int, *Response) { + total := TotalSpends{} + var conversions []Conversion -func (data SellCoinData) String() string { - return fmt.Sprintf("SELL COIN sell:%s %s buy:%s", - data.ValueToSell.String(), data.CoinToBuy.String(), data.CoinToSell.String()) -} + commissionInBaseCoin := tx.CommissionInBaseCoin() + commissionIncluded := false -func (data SellCoinData) Gas() int64 { - return commissions.ConvertTx + var value *big.Int + + if data.CoinToSell.IsBaseCoin() { + coin := context.GetStateCoin(data.CoinToBuy).Data() + + value = formula.CalculatePurchaseReturn(coin.Volume, coin.ReserveBalance, coin.Crr, data.ValueToSell) + total.Add(data.CoinToSell, value) + conversions = append(conversions, Conversion{ + FromCoin: data.CoinToSell, + ToCoin: data.CoinToBuy, + ToAmount: value, + ToReserve: data.ValueToSell, + }) + } else if data.CoinToBuy.IsBaseCoin() { + coin := context.GetStateCoin(data.CoinToSell).Data() + value = formula.CalculateSaleReturn(coin.Volume, coin.ReserveBalance, coin.Crr, data.ValueToSell) + rValue := big.NewInt(0).Set(value) + valueToSell := data.ValueToSell + + if tx.GasCoin == data.CoinToSell { + commissionIncluded = true + + newVolume := big.NewInt(0).Set(coin.Volume) + newReserve := big.NewInt(0).Set(coin.ReserveBalance) + + newVolume.Sub(newVolume, data.ValueToSell) + newReserve.Sub(newVolume, value) + + c := formula.CalculateSaleAmount(newVolume, newReserve, coin.Crr, commissionInBaseCoin) + + valueToSell.Add(valueToSell, c) + rValue.Add(rValue, commissionInBaseCoin) + } + + total.Add(data.CoinToSell, rValue) + conversions = append(conversions, Conversion{ + FromCoin: data.CoinToSell, + FromAmount: valueToSell, + FromReserve: rValue, + ToCoin: data.CoinToBuy, + }) + } else { + coinFrom := context.GetStateCoin(data.CoinToSell).Data() + coinTo := context.GetStateCoin(data.CoinToBuy).Data() + + valueToSell := big.NewInt(0).Set(data.ValueToSell) + + basecoinValue := formula.CalculateSaleReturn(coinFrom.Volume, coinFrom.ReserveBalance, coinFrom.Crr, data.ValueToSell) + + if tx.GasCoin == data.CoinToSell { + commissionIncluded = true + newVolume := big.NewInt(0).Set(coinFrom.Volume) + newReserve := big.NewInt(0).Set(coinFrom.ReserveBalance) + + newVolume.Sub(newVolume, data.ValueToSell) + newReserve.Sub(newVolume, value) + + c := formula.CalculateSaleAmount(newVolume, newReserve, coinFrom.Crr, commissionInBaseCoin) + + valueToSell.Add(valueToSell, c) + basecoinValue.Add(basecoinValue, commissionInBaseCoin) + } + + value = formula.CalculatePurchaseReturn(coinTo.Volume, coinTo.ReserveBalance, coinTo.Crr, basecoinValue) + + total.Add(data.CoinToSell, value) + + conversions = append(conversions, Conversion{ + FromCoin: data.CoinToSell, + FromAmount: valueToSell, + FromReserve: basecoinValue, + ToCoin: data.CoinToBuy, + ToAmount: value, + ToReserve: basecoinValue, + }) + } + + if !commissionIncluded { + commission := big.NewInt(0).Set(commissionInBaseCoin) + + if !tx.GasCoin.IsBaseCoin() { + coin := context.GetStateCoin(tx.GasCoin) + + if coin.ReserveBalance().Cmp(commissionInBaseCoin) < 0 { + return nil, nil, nil, &Response{ + Code: code.CoinReserveNotSufficient, + Log: fmt.Sprintf("Gas coin reserve balance is not sufficient for transaction. Has: %s %s, required %s %s", + coin.ReserveBalance().String(), + types.GetBaseCoin(), + commissionInBaseCoin.String(), + types.GetBaseCoin())} + } + + commission = formula.CalculateSaleAmount(coin.Volume(), coin.ReserveBalance(), coin.Data().Crr, commissionInBaseCoin) + } + + total.Add(tx.GasCoin, commission) + } + + return total, conversions, value, nil } -func (data SellCoinData) Run(sender types.Address, tx *Transaction, context *state.StateDB, isCheck bool, rewardPool *big.Int, currentBlock int64) Response { +func (data SellCoinData) BasicCheck(tx *Transaction, context *state.StateDB) *Response { if data.CoinToSell == data.CoinToBuy { - return Response{ + return &Response{ Code: code.CrossConvert, Log: fmt.Sprintf("\"From\" coin equals to \"to\" coin")} } if !context.CoinExists(data.CoinToSell) { - return Response{ + return &Response{ Code: code.CoinNotExists, Log: fmt.Sprintf("Coin not exists")} } if !context.CoinExists(data.CoinToBuy) { - return Response{ + return &Response{ Code: code.CoinNotExists, Log: fmt.Sprintf("Coin not exists")} } if !context.CoinExists(tx.GasCoin) { - return Response{ + return &Response{ Code: code.CoinNotExists, Log: fmt.Sprintf("Coin %s not exists", tx.GasCoin)} } - commissionInBaseCoin := big.NewInt(0).Mul(tx.GasPrice, big.NewInt(tx.Gas())) - commissionInBaseCoin.Mul(commissionInBaseCoin, CommissionMultiplier) - commission := big.NewInt(0).Set(commissionInBaseCoin) - - if !tx.GasCoin.IsBaseCoin() { - coin := context.GetStateCoin(tx.GasCoin) - - if coin.ReserveBalance().Cmp(commissionInBaseCoin) < 0 { - return Response{ - Code: code.CoinReserveNotSufficient, - Log: fmt.Sprintf("Coin reserve balance is not sufficient for transaction. Has: %s, required %s", coin.ReserveBalance().String(), commissionInBaseCoin.String())} - } - - commission = formula.CalculateSaleAmount(coin.Volume(), coin.ReserveBalance(), coin.Data().Crr, commissionInBaseCoin) + return nil +} - if commission == nil { - return Response{ - Code: 999, - Log: "Unknown error"} - } - } +func (data SellCoinData) String() string { + return fmt.Sprintf("SELL COIN sell:%s %s buy:%s", + data.ValueToSell.String(), data.CoinToBuy.String(), data.CoinToSell.String()) +} - if context.GetBalance(sender, data.CoinToSell).Cmp(data.ValueToSell) < 0 { - return Response{ - Code: code.InsufficientFunds, - Log: fmt.Sprintf("Insufficient funds for sender account: %s. Wanted %d ", sender.String(), data.ValueToSell)} - } +func (data SellCoinData) Gas() int64 { + return commissions.ConvertTx +} - if data.CoinToSell == tx.GasCoin { - totalTxCost := big.NewInt(0) - totalTxCost.Add(totalTxCost, data.ValueToSell) - totalTxCost.Add(totalTxCost, commission) +func (data SellCoinData) Run(tx *Transaction, context *state.StateDB, isCheck bool, rewardPool *big.Int, currentBlock int64) Response { + sender, _ := tx.Sender() - if context.GetBalance(sender, tx.GasCoin).Cmp(totalTxCost) < 0 { - return Response{ - Code: code.InsufficientFunds, - Log: fmt.Sprintf("Insufficient funds for sender account: %s. Wanted %s %s", sender.String(), totalTxCost.String(), tx.GasCoin)} - } + response := data.BasicCheck(tx, context) + if response != nil { + return *response } - if !isCheck { - rewardPool.Add(rewardPool, commissionInBaseCoin) - - context.SubBalance(sender, data.CoinToSell, data.ValueToSell) - context.SubBalance(sender, tx.GasCoin, commission) - - if !tx.GasCoin.IsBaseCoin() { - context.SubCoinVolume(tx.GasCoin, commission) - context.SubCoinReserve(tx.GasCoin, commissionInBaseCoin) - } + totalSpends, conversions, value, response := data.TotalSpend(tx, context) + if response != nil { + return *response } - var value *big.Int - if data.CoinToSell.IsBaseCoin() { coin := context.GetStateCoin(data.CoinToBuy).Data() - value = formula.CalculatePurchaseReturn(coin.Volume, coin.ReserveBalance, coin.Crr, data.ValueToSell) if !isCheck { @@ -130,7 +187,6 @@ func (data SellCoinData) Run(sender types.Address, tx *Transaction, context *sta } } else if data.CoinToBuy.IsBaseCoin() { coin := context.GetStateCoin(data.CoinToSell).Data() - value = formula.CalculateSaleReturn(coin.Volume, coin.ReserveBalance, coin.Crr, data.ValueToSell) if !isCheck { @@ -153,7 +209,31 @@ func (data SellCoinData) Run(sender types.Address, tx *Transaction, context *sta } } + for _, ts := range totalSpends { + if context.GetBalance(sender, ts.Coin).Cmp(ts.Value) < 0 { + return Response{ + Code: code.InsufficientFunds, + Log: fmt.Sprintf("Insufficient funds for sender account: %s. Wanted %s %s.", + sender.String(), + ts.Value.String(), + ts.Coin)} + } + } + if !isCheck { + for _, ts := range totalSpends { + context.SubBalance(sender, ts.Coin, ts.Value) + } + + for _, conversion := range conversions { + context.SubCoinVolume(conversion.FromCoin, conversion.FromAmount) + context.SubCoinReserve(conversion.FromCoin, conversion.FromReserve) + + context.AddCoinVolume(conversion.ToCoin, conversion.ToAmount) + context.AddCoinReserve(conversion.ToCoin, conversion.ToReserve) + } + + rewardPool.Add(rewardPool, tx.CommissionInBaseCoin()) context.AddBalance(sender, data.CoinToBuy, value) context.SetNonce(sender, tx.Nonce) } diff --git a/core/transaction/send.go b/core/transaction/send.go index cce9dac25..95ee0450e 100644 --- a/core/transaction/send.go +++ b/core/transaction/send.go @@ -22,7 +22,7 @@ func (data SendData) TotalSpend(tx *Transaction, context *state.StateDB) (TotalS total := TotalSpends{} var conversions []Conversion - commissionInBaseCoin := data.CommissionInBaseCoin(tx) + commissionInBaseCoin := tx.CommissionInBaseCoin() commission := big.NewInt(0).Set(commissionInBaseCoin) if !tx.GasCoin.IsBaseCoin() { @@ -50,13 +50,6 @@ func (data SendData) TotalSpend(tx *Transaction, context *state.StateDB) (TotalS return total, conversions, nil, nil } -func (data SendData) CommissionInBaseCoin(tx *Transaction) *big.Int { - commissionInBaseCoin := big.NewInt(0).Mul(tx.GasPrice, big.NewInt(tx.Gas())) - commissionInBaseCoin.Mul(commissionInBaseCoin, CommissionMultiplier) - - return commissionInBaseCoin -} - func (data SendData) BasicCheck(tx *Transaction, context *state.StateDB) *Response { if !context.CoinExists(data.Coin) { return &Response{ @@ -119,7 +112,7 @@ func (data SendData) Run(tx *Transaction, context *state.StateDB, isCheck bool, context.AddCoinReserve(conversion.ToCoin, conversion.ToReserve) } - rewardPool.Add(rewardPool, data.CommissionInBaseCoin(tx)) + rewardPool.Add(rewardPool, tx.CommissionInBaseCoin()) context.AddBalance(data.To, data.Coin, data.Value) context.SetNonce(sender, tx.Nonce) } diff --git a/core/transaction/transaction.go b/core/transaction/transaction.go index f046cef71..55383a537 100644 --- a/core/transaction/transaction.go +++ b/core/transaction/transaction.go @@ -100,7 +100,6 @@ type Data interface { String() string Gas() int64 TotalSpend(tx *Transaction, context *state.StateDB) (TotalSpends, []Conversion, *big.Int, *Response) - CommissionInBaseCoin(tx *Transaction) *big.Int BasicCheck(tx *Transaction, context *state.StateDB) *Response Run(tx *Transaction, context *state.StateDB, isCheck bool, rewardPool *big.Int, currentBlock int64) Response } @@ -117,6 +116,13 @@ func (tx *Transaction) payloadGas() int64 { return int64(len(tx.Payload)+len(tx.ServiceData)) * commissions.PayloadByte } +func (tx *Transaction) CommissionInBaseCoin() *big.Int { + commissionInBaseCoin := big.NewInt(0).Mul(tx.GasPrice, big.NewInt(tx.Gas())) + commissionInBaseCoin.Mul(commissionInBaseCoin, CommissionMultiplier) + + return commissionInBaseCoin +} + func (tx *Transaction) String() string { sender, _ := tx.Sender() From 6a586ada7e22c888cbdfa7a53be904a9d9b950b8 Mon Sep 17 00:00:00 2001 From: Daniil Lashin Date: Mon, 3 Dec 2018 13:03:22 +0300 Subject: [PATCH 08/49] Refactor SellAllCoin tx --- core/transaction/sell_all_coin.go | 56 ++++++++++++++----------------- 1 file changed, 26 insertions(+), 30 deletions(-) diff --git a/core/transaction/sell_all_coin.go b/core/transaction/sell_all_coin.go index 8e1850d3a..43159b2c2 100644 --- a/core/transaction/sell_all_coin.go +++ b/core/transaction/sell_all_coin.go @@ -2,7 +2,6 @@ package transaction import ( "encoding/hex" - "encoding/json" "fmt" "github.com/MinterTeam/minter-go-node/core/code" "github.com/MinterTeam/minter-go-node/core/commissions" @@ -18,44 +17,49 @@ type SellAllCoinData struct { CoinToBuy types.CoinSymbol } -func (data SellAllCoinData) MarshalJSON() ([]byte, error) { - return json.Marshal(struct { - CoinToSell types.CoinSymbol `json:"coin_to_sell,string"` - CoinToBuy types.CoinSymbol `json:"coin_to_buy,string"` - }{ - CoinToSell: data.CoinToSell, - CoinToBuy: data.CoinToBuy, - }) +func (data SellAllCoinData) TotalSpend(tx *Transaction, context *state.StateDB) (TotalSpends, []Conversion, *big.Int, *Response) { + panic("implement me") } -func (data SellAllCoinData) String() string { - return fmt.Sprintf("SELL ALL COIN sell:%s buy:%s", - data.CoinToSell.String(), data.CoinToBuy.String()) -} - -func (data SellAllCoinData) Gas() int64 { - return commissions.ConvertTx -} - -func (data SellAllCoinData) Run(sender types.Address, tx *Transaction, context *state.StateDB, isCheck bool, rewardPool *big.Int, currentBlock int64) Response { +func (data SellAllCoinData) BasicCheck(tx *Transaction, context *state.StateDB) *Response { if data.CoinToSell == data.CoinToBuy { - return Response{ + return &Response{ Code: code.CrossConvert, Log: fmt.Sprintf("\"From\" coin equals to \"to\" coin")} } if !context.CoinExists(data.CoinToSell) { - return Response{ + return &Response{ Code: code.CoinNotExists, Log: fmt.Sprintf("Coin not exists")} } if !context.CoinExists(data.CoinToBuy) { - return Response{ + return &Response{ Code: code.CoinNotExists, Log: fmt.Sprintf("Coin not exists")} } + return nil +} + +func (data SellAllCoinData) String() string { + return fmt.Sprintf("SELL ALL COIN sell:%s buy:%s", + data.CoinToSell.String(), data.CoinToBuy.String()) +} + +func (data SellAllCoinData) Gas() int64 { + return commissions.ConvertTx +} + +func (data SellAllCoinData) Run(tx *Transaction, context *state.StateDB, isCheck bool, rewardPool *big.Int, currentBlock int64) Response { + sender, _ := tx.Sender() + + response := data.BasicCheck(tx, context) + if response != nil { + return *response + } + available := context.GetBalance(sender, data.CoinToSell) commissionInBaseCoin := big.NewInt(0).Mul(tx.GasPrice, big.NewInt(tx.Gas())) @@ -72,12 +76,6 @@ func (data SellAllCoinData) Run(sender types.Address, tx *Transaction, context * } commission = formula.CalculateSaleAmount(coin.Volume(), coin.ReserveBalance(), coin.Data().Crr, commissionInBaseCoin) - - if commission == nil { - return Response{ - Code: 999, - Log: "Unknown error"} - } } if context.GetBalance(sender, data.CoinToSell).Cmp(commission) < 0 { @@ -104,7 +102,6 @@ func (data SellAllCoinData) Run(sender types.Address, tx *Transaction, context * if data.CoinToSell.IsBaseCoin() { coin := context.GetStateCoin(data.CoinToBuy).Data() - value = formula.CalculatePurchaseReturn(coin.Volume, coin.ReserveBalance, coin.Crr, amountToSell) if !isCheck { @@ -113,7 +110,6 @@ func (data SellAllCoinData) Run(sender types.Address, tx *Transaction, context * } } else if data.CoinToBuy.IsBaseCoin() { coin := context.GetStateCoin(data.CoinToSell).Data() - value = formula.CalculateSaleReturn(coin.Volume, coin.ReserveBalance, coin.Crr, amountToSell) if !isCheck { From e170142c051fe60d4e15df97e98b6930d816852e Mon Sep 17 00:00:00 2001 From: Daniil Lashin Date: Mon, 3 Dec 2018 13:05:57 +0300 Subject: [PATCH 09/49] Refactor CreateCoin tx --- core/transaction/create_coin.go | 91 +++++++++++++++------------------ 1 file changed, 41 insertions(+), 50 deletions(-) diff --git a/core/transaction/create_coin.go b/core/transaction/create_coin.go index c920008dd..ca1537f28 100644 --- a/core/transaction/create_coin.go +++ b/core/transaction/create_coin.go @@ -2,7 +2,6 @@ package transaction import ( "encoding/hex" - "encoding/json" "fmt" "github.com/MinterTeam/minter-go-node/core/code" "github.com/MinterTeam/minter-go-node/core/commissions" @@ -25,20 +24,42 @@ type CreateCoinData struct { ConstantReserveRatio uint } -func (data CreateCoinData) MarshalJSON() ([]byte, error) { - return json.Marshal(struct { - Name string `json:"name"` - Symbol types.CoinSymbol `json:"coin_symbol"` - InitialAmount string `json:"initial_amount"` - InitialReserve string `json:"initial_reserve"` - ConstantReserveRatio uint `json:"constant_reserve_ratio"` - }{ - Name: data.Name, - Symbol: data.Symbol, - InitialAmount: data.InitialAmount.String(), - InitialReserve: data.InitialReserve.String(), - ConstantReserveRatio: data.ConstantReserveRatio, - }) +func (data CreateCoinData) TotalSpend(tx *Transaction, context *state.StateDB) (TotalSpends, []Conversion, *big.Int, *Response) { + panic("implement me") +} + +func (data CreateCoinData) BasicCheck(tx *Transaction, context *state.StateDB) *Response { + if !context.CoinExists(tx.GasCoin) { + return &Response{ + Code: code.CoinNotExists, + Log: fmt.Sprintf("Coin %s not exists", tx.GasCoin)} + } + + if len(data.Name) > maxCoinNameBytes { + return &Response{ + Code: code.InvalidCoinName, + Log: fmt.Sprintf("Coin name is invalid. Allowed up to %d bytes.", maxCoinNameBytes)} + } + + if match, _ := regexp.MatchString(allowedCoinSymbols, data.Symbol.String()); !match { + return &Response{ + Code: code.InvalidCoinSymbol, + Log: fmt.Sprintf("Invalid coin symbol. Should be %s", allowedCoinSymbols)} + } + + if context.CoinExists(data.Symbol) { + return &Response{ + Code: code.CoinAlreadyExists, + Log: fmt.Sprintf("Coin already exists")} + } + + if data.ConstantReserveRatio < 10 || data.ConstantReserveRatio > 100 { + return &Response{ + Code: code.WrongCrr, + Log: fmt.Sprintf("Constant Reserve Ratio should be between 10 and 100")} + } + + return nil } func (data CreateCoinData) String() string { @@ -68,24 +89,12 @@ func (data CreateCoinData) Gas() int64 { return gas } -func (data CreateCoinData) Run(sender types.Address, tx *Transaction, context *state.StateDB, isCheck bool, rewardPool *big.Int, currentBlock int64) Response { - - if !context.CoinExists(tx.GasCoin) { - return Response{ - Code: code.CoinNotExists, - Log: fmt.Sprintf("Coin %s not exists", tx.GasCoin)} - } - - if len(data.Name) > maxCoinNameBytes { - return Response{ - Code: code.InvalidCoinName, - Log: fmt.Sprintf("Coin name is invalid. Allowed up to %d bytes.", maxCoinNameBytes)} - } +func (data CreateCoinData) Run(tx *Transaction, context *state.StateDB, isCheck bool, rewardPool *big.Int, currentBlock int64) Response { + sender, _ := tx.Sender() - if match, _ := regexp.MatchString(allowedCoinSymbols, data.Symbol.String()); !match { - return Response{ - Code: code.InvalidCoinSymbol, - Log: fmt.Sprintf("Invalid coin symbol. Should be %s", allowedCoinSymbols)} + response := data.BasicCheck(tx, context) + if response != nil { + return *response } commissionInBaseCoin := big.NewInt(0).Mul(tx.GasPrice, big.NewInt(tx.Gas())) @@ -102,12 +111,6 @@ func (data CreateCoinData) Run(sender types.Address, tx *Transaction, context *s } commission = formula.CalculateSaleAmount(coin.Volume(), coin.ReserveBalance(), coin.Data().Crr, commissionInBaseCoin) - - if commission == nil { - return Response{ - Code: 999, - Log: "Unknown error"} - } } if context.GetBalance(sender, tx.GasCoin).Cmp(commission) < 0 { @@ -134,18 +137,6 @@ func (data CreateCoinData) Run(sender types.Address, tx *Transaction, context *s } } - if context.CoinExists(data.Symbol) { - return Response{ - Code: code.CoinAlreadyExists, - Log: fmt.Sprintf("Coin already exists")} - } - - if data.ConstantReserveRatio < 10 || data.ConstantReserveRatio > 100 { - return Response{ - Code: code.WrongCrr, - Log: fmt.Sprintf("Constant Reserve Ratio should be between 10 and 100")} - } - if !isCheck { rewardPool.Add(rewardPool, commissionInBaseCoin) From 0633076c6128526564be48eb328ed39175cd7b73 Mon Sep 17 00:00:00 2001 From: Daniil Lashin Date: Mon, 3 Dec 2018 13:07:46 +0300 Subject: [PATCH 10/49] Refactor DeclareCandidacy tx --- core/transaction/declare_candidacy.go | 79 ++++++++++++--------------- 1 file changed, 35 insertions(+), 44 deletions(-) diff --git a/core/transaction/declare_candidacy.go b/core/transaction/declare_candidacy.go index 869ed7164..985b1a783 100644 --- a/core/transaction/declare_candidacy.go +++ b/core/transaction/declare_candidacy.go @@ -1,7 +1,6 @@ package transaction import ( - "encoding/json" "fmt" "github.com/MinterTeam/minter-go-node/core/code" "github.com/MinterTeam/minter-go-node/core/commissions" @@ -24,20 +23,36 @@ type DeclareCandidacyData struct { Stake *big.Int } -func (data DeclareCandidacyData) MarshalJSON() ([]byte, error) { - return json.Marshal(struct { - Address types.Address `json:"address"` - PubKey string `json:"pub_key"` - Commission uint `json:"commission"` - Coin types.CoinSymbol `json:"coin"` - Stake string `json:"stake"` - }{ - Address: data.Address, - PubKey: fmt.Sprintf("Mp%x", data.PubKey), - Commission: data.Commission, - Coin: data.Coin, - Stake: data.Stake.String(), - }) +func (data DeclareCandidacyData) TotalSpend(tx *Transaction, context *state.StateDB) (TotalSpends, []Conversion, *big.Int, *Response) { + panic("implement me") +} + +func (data DeclareCandidacyData) BasicCheck(tx *Transaction, context *state.StateDB) *Response { + if !context.CoinExists(tx.GasCoin) { + return &Response{ + Code: code.CoinNotExists, + Log: fmt.Sprintf("Coin %s not exists", tx.GasCoin)} + } + + if len(data.PubKey) != 32 { + return &Response{ + Code: code.IncorrectPubKey, + Log: fmt.Sprintf("Incorrect PubKey")} + } + + if context.CandidateExists(data.PubKey) { + return &Response{ + Code: code.CandidateExists, + Log: fmt.Sprintf("Candidate with such public key (%x) already exists", data.PubKey)} + } + + if data.Commission < minCommission || data.Commission > maxCommission { + return &Response{ + Code: code.WrongCommission, + Log: fmt.Sprintf("Commission should be between 0 and 100")} + } + + return nil } func (data DeclareCandidacyData) String() string { @@ -49,18 +64,12 @@ func (data DeclareCandidacyData) Gas() int64 { return commissions.DeclareCandidacyTx } -func (data DeclareCandidacyData) Run(sender types.Address, tx *Transaction, context *state.StateDB, isCheck bool, rewardPool *big.Int, currentBlock int64) Response { +func (data DeclareCandidacyData) Run(tx *Transaction, context *state.StateDB, isCheck bool, rewardPool *big.Int, currentBlock int64) Response { + sender, _ := tx.Sender() - if !context.CoinExists(tx.GasCoin) { - return Response{ - Code: code.CoinNotExists, - Log: fmt.Sprintf("Coin %s not exists", tx.GasCoin)} - } - - if len(data.PubKey) != 32 { - return Response{ - Code: code.IncorrectPubKey, - Log: fmt.Sprintf("Incorrect PubKey")} + response := data.BasicCheck(tx, context) + if response != nil { + return *response } maxCandidatesCount := validators.GetCandidatesCountForBlock(currentBlock) @@ -85,12 +94,6 @@ func (data DeclareCandidacyData) Run(sender types.Address, tx *Transaction, cont } commission = formula.CalculateSaleAmount(coin.Volume(), coin.ReserveBalance(), coin.Data().Crr, commissionInBaseCoin) - - if commission == nil { - return Response{ - Code: 999, - Log: "Unknown error"} - } } if context.GetBalance(sender, data.Coin).Cmp(data.Stake) < 0 { @@ -117,18 +120,6 @@ func (data DeclareCandidacyData) Run(sender types.Address, tx *Transaction, cont } } - if context.CandidateExists(data.PubKey) { - return Response{ - Code: code.CandidateExists, - Log: fmt.Sprintf("Candidate with such public key (%x) already exists", data.PubKey)} - } - - if data.Commission < minCommission || data.Commission > maxCommission { - return Response{ - Code: code.WrongCommission, - Log: fmt.Sprintf("Commission should be between 0 and 100")} - } - if !isCheck { rewardPool.Add(rewardPool, commissionInBaseCoin) From e9c88d31649c36e04914e759374516442e65b0a7 Mon Sep 17 00:00:00 2001 From: Daniil Lashin Date: Mon, 3 Dec 2018 13:09:43 +0300 Subject: [PATCH 11/49] Refactor Delegate tx --- core/transaction/delegate.go | 80 ++++++++++++++++-------------------- 1 file changed, 35 insertions(+), 45 deletions(-) diff --git a/core/transaction/delegate.go b/core/transaction/delegate.go index 4737ac462..63a481ab1 100644 --- a/core/transaction/delegate.go +++ b/core/transaction/delegate.go @@ -1,7 +1,6 @@ package transaction import ( - "encoding/json" "fmt" "github.com/MinterTeam/minter-go-node/core/code" "github.com/MinterTeam/minter-go-node/core/commissions" @@ -18,43 +17,53 @@ type DelegateData struct { Stake *big.Int } -func (data DelegateData) MarshalJSON() ([]byte, error) { - return json.Marshal(struct { - PubKey string `json:"pub_key"` - Coin types.CoinSymbol `json:"coin"` - Stake string `json:"stake"` - }{ - PubKey: fmt.Sprintf("Mp%x", data.PubKey), - Coin: data.Coin, - Stake: data.Stake.String(), - }) +func (data DelegateData) TotalSpend(tx *Transaction, context *state.StateDB) (TotalSpends, []Conversion, *big.Int, *Response) { + panic("implement me") } -func (data DelegateData) String() string { - return fmt.Sprintf("DELEGATE pubkey:%s ", - hexutil.Encode(data.PubKey)) -} - -func (data DelegateData) Gas() int64 { - return commissions.DelegateTx -} - -func (data DelegateData) Run(sender types.Address, tx *Transaction, context *state.StateDB, isCheck bool, rewardPool *big.Int, currentBlock int64) Response { - +func (data DelegateData) BasicCheck(tx *Transaction, context *state.StateDB) *Response { if !context.CoinExists(tx.GasCoin) { - return Response{ + return &Response{ Code: code.CoinNotExists, Log: fmt.Sprintf("Coin %s not exists", tx.GasCoin)} } if data.Stake.Cmp(types.Big0) < 1 { - return Response{ + return &Response{ Code: code.StakeShouldBePositive, Log: fmt.Sprintf("Stake should be positive")} } - commissionInBaseCoin := big.NewInt(0).Mul(tx.GasPrice, big.NewInt(tx.Gas())) - commissionInBaseCoin.Mul(commissionInBaseCoin, CommissionMultiplier) + candidate := context.GetStateCandidate(data.PubKey) + if candidate == nil { + return &Response{ + Code: code.CandidateNotFound, + Log: fmt.Sprintf("Candidate with such public key not found")} + } + + sender, _ := tx.Sender() + if len(candidate.Stakes) >= state.MaxDelegatorsPerCandidate && !context.IsDelegatorStakeSufficient(sender, data.PubKey, data.Coin, data.Stake) { + return &Response{ + Code: code.TooLowStake, + Log: fmt.Sprintf("Stake is too low")} + } + + return nil +} + +func (data DelegateData) String() string { + return fmt.Sprintf("DELEGATE pubkey:%s ", + hexutil.Encode(data.PubKey)) +} + +func (data DelegateData) Gas() int64 { + return commissions.DelegateTx +} + +func (data DelegateData) Run(tx *Transaction, context *state.StateDB, isCheck bool, rewardPool *big.Int, currentBlock int64) Response { + sender, _ := tx.Sender() + + commissionInBaseCoin := tx.CommissionInBaseCoin() commission := big.NewInt(0).Set(commissionInBaseCoin) if tx.GasCoin != types.GetBaseCoin() { @@ -67,12 +76,6 @@ func (data DelegateData) Run(sender types.Address, tx *Transaction, context *sta } commission = formula.CalculateSaleAmount(coin.Volume(), coin.ReserveBalance(), coin.Data().Crr, commissionInBaseCoin) - - if commission == nil { - return Response{ - Code: 999, - Log: "Unknown error"} - } } if context.GetBalance(sender, tx.GasCoin).Cmp(commission) < 0 { @@ -99,19 +102,6 @@ func (data DelegateData) Run(sender types.Address, tx *Transaction, context *sta } } - candidate := context.GetStateCandidate(data.PubKey) - if candidate == nil { - return Response{ - Code: code.CandidateNotFound, - Log: fmt.Sprintf("Candidate with such public key not found")} - } - - if len(candidate.Stakes) >= state.MaxDelegatorsPerCandidate && !context.IsDelegatorStakeSufficient(sender, data.PubKey, data.Coin, data.Stake) { - return Response{ - Code: code.TooLowStake, - Log: fmt.Sprintf("Stake is too low")} - } - if !isCheck { rewardPool.Add(rewardPool, commissionInBaseCoin) From 193c8f9cf4fd3ee737a6f52f1e2dc7bb3b746ccf Mon Sep 17 00:00:00 2001 From: Daniil Lashin Date: Mon, 3 Dec 2018 13:12:32 +0300 Subject: [PATCH 12/49] Refactor SetCandidateOn/SetCandidateOff tx --- core/transaction/delegate.go | 5 + core/transaction/switch_candidate_status.go | 130 ++++++++++---------- 2 files changed, 67 insertions(+), 68 deletions(-) diff --git a/core/transaction/delegate.go b/core/transaction/delegate.go index 63a481ab1..621c901d9 100644 --- a/core/transaction/delegate.go +++ b/core/transaction/delegate.go @@ -63,6 +63,11 @@ func (data DelegateData) Gas() int64 { func (data DelegateData) Run(tx *Transaction, context *state.StateDB, isCheck bool, rewardPool *big.Int, currentBlock int64) Response { sender, _ := tx.Sender() + response := data.BasicCheck(tx, context) + if response != nil { + return *response + } + commissionInBaseCoin := tx.CommissionInBaseCoin() commission := big.NewInt(0).Set(commissionInBaseCoin) diff --git a/core/transaction/switch_candidate_status.go b/core/transaction/switch_candidate_status.go index 3e5e37633..86c811356 100644 --- a/core/transaction/switch_candidate_status.go +++ b/core/transaction/switch_candidate_status.go @@ -2,7 +2,6 @@ package transaction import ( "bytes" - "encoding/json" "fmt" "github.com/MinterTeam/minter-go-node/core/code" "github.com/MinterTeam/minter-go-node/core/commissions" @@ -16,12 +15,32 @@ type SetCandidateOnData struct { PubKey []byte } -func (data SetCandidateOnData) MarshalJSON() ([]byte, error) { - return json.Marshal(struct { - PubKey string `json:"pub_key"` - }{ - PubKey: fmt.Sprintf("Mp%x", data.PubKey), - }) +func (data SetCandidateOnData) TotalSpend(tx *Transaction, context *state.StateDB) (TotalSpends, []Conversion, *big.Int, *Response) { + panic("implement me") +} + +func (data SetCandidateOnData) BasicCheck(tx *Transaction, context *state.StateDB) *Response { + if !context.CoinExists(tx.GasCoin) { + return &Response{ + Code: code.CoinNotExists, + Log: fmt.Sprintf("Coin %s not exists", tx.GasCoin)} + } + + if !context.CandidateExists(data.PubKey) { + return &Response{ + Code: code.CandidateNotFound, + Log: fmt.Sprintf("Candidate with such public key (%x) not found", data.PubKey)} + } + + candidate := context.GetStateCandidate(data.PubKey) + sender, _ := tx.Sender() + if !bytes.Equal(candidate.CandidateAddress.Bytes(), sender.Bytes()) { + return &Response{ + Code: code.IsNotOwnerOfCandidate, + Log: fmt.Sprintf("Sender is not an owner of a candidate")} + } + + return nil } func (data SetCandidateOnData) String() string { @@ -33,15 +52,15 @@ func (data SetCandidateOnData) Gas() int64 { return commissions.ToggleCandidateStatus } -func (data SetCandidateOnData) Run(sender types.Address, tx *Transaction, context *state.StateDB, isCheck bool, rewardPool *big.Int, currentBlock int64) Response { - if !context.CoinExists(tx.GasCoin) { - return Response{ - Code: code.CoinNotExists, - Log: fmt.Sprintf("Coin %s not exists", tx.GasCoin)} +func (data SetCandidateOnData) Run(tx *Transaction, context *state.StateDB, isCheck bool, rewardPool *big.Int, currentBlock int64) Response { + sender, _ := tx.Sender() + + response := data.BasicCheck(tx, context) + if response != nil { + return *response } - commissionInBaseCoin := big.NewInt(0).Mul(tx.GasPrice, big.NewInt(tx.Gas())) - commissionInBaseCoin.Mul(commissionInBaseCoin, CommissionMultiplier) + commissionInBaseCoin := tx.CommissionInBaseCoin() commission := big.NewInt(0).Set(commissionInBaseCoin) if tx.GasCoin != types.GetBaseCoin() { @@ -54,12 +73,6 @@ func (data SetCandidateOnData) Run(sender types.Address, tx *Transaction, contex } commission = formula.CalculateSaleAmount(coin.Volume(), coin.ReserveBalance(), coin.Data().Crr, commissionInBaseCoin) - - if commission == nil { - return Response{ - Code: 999, - Log: "Unknown error"} - } } if context.GetBalance(sender, tx.GasCoin).Cmp(commission) < 0 { @@ -68,20 +81,6 @@ func (data SetCandidateOnData) Run(sender types.Address, tx *Transaction, contex Log: fmt.Sprintf("Insufficient funds for sender account: %s. Wanted %s %s", sender.String(), commission, tx.GasCoin)} } - if !context.CandidateExists(data.PubKey) { - return Response{ - Code: code.CandidateNotFound, - Log: fmt.Sprintf("Candidate with such public key (%x) not found", data.PubKey)} - } - - candidate := context.GetStateCandidate(data.PubKey) - - if !bytes.Equal(candidate.CandidateAddress.Bytes(), sender.Bytes()) { - return Response{ - Code: code.IsNotOwnerOfCandidate, - Log: fmt.Sprintf("Sender is not an owner of a candidate")} - } - if !isCheck { rewardPool.Add(rewardPool, commissionInBaseCoin) @@ -101,12 +100,32 @@ type SetCandidateOffData struct { PubKey []byte } -func (data SetCandidateOffData) MarshalJSON() ([]byte, error) { - return json.Marshal(struct { - PubKey string `json:"pub_key"` - }{ - PubKey: fmt.Sprintf("Mp%x", data.PubKey), - }) +func (data SetCandidateOffData) TotalSpend(tx *Transaction, context *state.StateDB) (TotalSpends, []Conversion, *big.Int, *Response) { + panic("implement me") +} + +func (data SetCandidateOffData) BasicCheck(tx *Transaction, context *state.StateDB) *Response { + if !context.CoinExists(tx.GasCoin) { + return &Response{ + Code: code.CoinNotExists, + Log: fmt.Sprintf("Coin %s not exists", tx.GasCoin)} + } + + if !context.CandidateExists(data.PubKey) { + return &Response{ + Code: code.CandidateNotFound, + Log: fmt.Sprintf("Candidate with such public key (%x) not found", data.PubKey)} + } + + candidate := context.GetStateCandidate(data.PubKey) + sender, _ := tx.Sender() + if !bytes.Equal(candidate.CandidateAddress.Bytes(), sender.Bytes()) { + return &Response{ + Code: code.IsNotOwnerOfCandidate, + Log: fmt.Sprintf("Sender is not an owner of a candidate")} + } + + return nil } func (data SetCandidateOffData) String() string { @@ -118,15 +137,10 @@ func (data SetCandidateOffData) Gas() int64 { return commissions.ToggleCandidateStatus } -func (data SetCandidateOffData) Run(sender types.Address, tx *Transaction, context *state.StateDB, isCheck bool, rewardPool *big.Int, currentBlock int64) Response { - if !context.CoinExists(tx.GasCoin) { - return Response{ - Code: code.CoinNotExists, - Log: fmt.Sprintf("Coin %s not exists", tx.GasCoin)} - } +func (data SetCandidateOffData) Run(tx *Transaction, context *state.StateDB, isCheck bool, rewardPool *big.Int, currentBlock int64) Response { + sender, _ := tx.Sender() - commissionInBaseCoin := big.NewInt(0).Mul(tx.GasPrice, big.NewInt(tx.Gas())) - commissionInBaseCoin.Mul(commissionInBaseCoin, CommissionMultiplier) + commissionInBaseCoin := tx.CommissionInBaseCoin() commission := big.NewInt(0).Set(commissionInBaseCoin) if tx.GasCoin != types.GetBaseCoin() { @@ -139,12 +153,6 @@ func (data SetCandidateOffData) Run(sender types.Address, tx *Transaction, conte } commission = formula.CalculateSaleAmount(coin.Volume(), coin.ReserveBalance(), coin.Data().Crr, commissionInBaseCoin) - - if commission == nil { - return Response{ - Code: 999, - Log: "Unknown error"} - } } if context.GetBalance(sender, tx.GasCoin).Cmp(commission) < 0 { @@ -153,20 +161,6 @@ func (data SetCandidateOffData) Run(sender types.Address, tx *Transaction, conte Log: fmt.Sprintf("Insufficient funds for sender account: %s. Wanted %s %s", sender.String(), commission, tx.GasCoin)} } - if !context.CandidateExists(data.PubKey) { - return Response{ - Code: code.CandidateNotFound, - Log: fmt.Sprintf("Candidate with such public key (%x) not found", data.PubKey)} - } - - candidate := context.GetStateCandidate(data.PubKey) - - if !bytes.Equal(candidate.CandidateAddress.Bytes(), sender.Bytes()) { - return Response{ - Code: code.IsNotOwnerOfCandidate, - Log: fmt.Sprintf("Sender is not an owner of a candidate")} - } - if !isCheck { rewardPool.Add(rewardPool, commissionInBaseCoin) From 1d0ed9dab5686a8997cfb7caf43073cfca376de8 Mon Sep 17 00:00:00 2001 From: Daniil Lashin Date: Mon, 3 Dec 2018 13:14:11 +0300 Subject: [PATCH 13/49] Refactor Unbond tx --- core/transaction/unbond.go | 87 ++++++++++++++++++-------------------- 1 file changed, 41 insertions(+), 46 deletions(-) diff --git a/core/transaction/unbond.go b/core/transaction/unbond.go index a5f48a4f1..0a7604250 100644 --- a/core/transaction/unbond.go +++ b/core/transaction/unbond.go @@ -1,7 +1,6 @@ package transaction import ( - "encoding/json" "fmt" "github.com/MinterTeam/minter-go-node/core/code" "github.com/MinterTeam/minter-go-node/core/commissions" @@ -20,16 +19,41 @@ type UnbondData struct { Value *big.Int } -func (data UnbondData) MarshalJSON() ([]byte, error) { - return json.Marshal(struct { - PubKey string `json:"pub_key"` - Coin types.CoinSymbol `json:"coin"` - Value string `json:"value"` - }{ - PubKey: fmt.Sprintf("Mp%x", data.PubKey), - Coin: data.Coin, - Value: data.Value.String(), - }) +func (data UnbondData) TotalSpend(tx *Transaction, context *state.StateDB) (TotalSpends, []Conversion, *big.Int, *Response) { + panic("implement me") +} + +func (data UnbondData) BasicCheck(tx *Transaction, context *state.StateDB) *Response { + if !context.CoinExists(tx.GasCoin) { + return &Response{ + Code: code.CoinNotExists, + Log: fmt.Sprintf("Coin %s not exists", tx.GasCoin)} + } + + if !context.CandidateExists(data.PubKey) { + return &Response{ + Code: code.CandidateNotFound, + Log: fmt.Sprintf("Candidate with such public key not found")} + } + + candidate := context.GetStateCandidate(data.PubKey) + + sender, _ := tx.Sender() + stake := candidate.GetStakeOfAddress(sender, data.Coin) + + if stake == nil { + return &Response{ + Code: code.StakeNotFound, + Log: fmt.Sprintf("Stake of current user not found")} + } + + if stake.Value.Cmp(data.Value) < 0 { + return &Response{ + Code: code.InsufficientStake, + Log: fmt.Sprintf("Insufficient stake for sender account")} + } + + return nil } func (data UnbondData) String() string { @@ -41,16 +65,15 @@ func (data UnbondData) Gas() int64 { return commissions.UnbondTx } -func (data UnbondData) Run(sender types.Address, tx *Transaction, context *state.StateDB, isCheck bool, rewardPool *big.Int, currentBlock int64) Response { +func (data UnbondData) Run(tx *Transaction, context *state.StateDB, isCheck bool, rewardPool *big.Int, currentBlock int64) Response { + sender, _ := tx.Sender() - if !context.CoinExists(tx.GasCoin) { - return Response{ - Code: code.CoinNotExists, - Log: fmt.Sprintf("Coin %s not exists", tx.GasCoin)} + response := data.BasicCheck(tx, context) + if response != nil { + return *response } - commissionInBaseCoin := big.NewInt(0).Mul(tx.GasPrice, big.NewInt(tx.Gas())) - commissionInBaseCoin.Mul(commissionInBaseCoin, CommissionMultiplier) + commissionInBaseCoin := tx.CommissionInBaseCoin() commission := big.NewInt(0).Set(commissionInBaseCoin) if tx.GasCoin != types.GetBaseCoin() { @@ -63,12 +86,6 @@ func (data UnbondData) Run(sender types.Address, tx *Transaction, context *state } commission = formula.CalculateSaleAmount(coin.Volume(), coin.ReserveBalance(), coin.Data().Crr, commissionInBaseCoin) - - if commission == nil { - return Response{ - Code: 999, - Log: "Unknown error"} - } } if context.GetBalance(sender, tx.GasCoin).Cmp(commission) < 0 { @@ -77,28 +94,6 @@ func (data UnbondData) Run(sender types.Address, tx *Transaction, context *state Log: fmt.Sprintf("Insufficient funds for sender account: %s. Wanted %s %s", sender.String(), commission, tx.GasCoin)} } - if !context.CandidateExists(data.PubKey) { - return Response{ - Code: code.CandidateNotFound, - Log: fmt.Sprintf("Candidate with such public key not found")} - } - - candidate := context.GetStateCandidate(data.PubKey) - - stake := candidate.GetStakeOfAddress(sender, data.Coin) - - if stake == nil { - return Response{ - Code: code.StakeNotFound, - Log: fmt.Sprintf("Stake of current user not found")} - } - - if stake.Value.Cmp(data.Value) < 0 { - return Response{ - Code: code.InsufficientStake, - Log: fmt.Sprintf("Insufficient stake for sender account")} - } - if !isCheck { // now + 30 days unbondAtBlock := uint64(currentBlock + unbondPeriod) From 579b100dd7c699091dab11a94d8108e9344418ad Mon Sep 17 00:00:00 2001 From: Daniil Lashin Date: Mon, 3 Dec 2018 13:15:28 +0300 Subject: [PATCH 14/49] Refactor CreateMultisig tx --- core/transaction/create_multisig.go | 62 ++++++++++++----------------- 1 file changed, 26 insertions(+), 36 deletions(-) diff --git a/core/transaction/create_multisig.go b/core/transaction/create_multisig.go index f64d871be..ef69a6bbf 100644 --- a/core/transaction/create_multisig.go +++ b/core/transaction/create_multisig.go @@ -2,7 +2,6 @@ package transaction import ( "encoding/hex" - "encoding/json" "fmt" "github.com/MinterTeam/minter-go-node/core/code" "github.com/MinterTeam/minter-go-node/core/commissions" @@ -19,41 +18,44 @@ type CreateMultisigData struct { Addresses []types.Address } -func (data CreateMultisigData) MarshalJSON() ([]byte, error) { - return json.Marshal(struct { - Threshold uint `json:"threshold"` - Weights []uint `json:"weights"` - Addresses []types.Address `json:"addresses"` - }{ - Weights: data.Weights, - Threshold: data.Threshold, - Addresses: data.Addresses, - }) -} - -func (data CreateMultisigData) String() string { - return fmt.Sprintf("CREATE MULTISIG") +func (data CreateMultisigData) TotalSpend(tx *Transaction, context *state.StateDB) (TotalSpends, []Conversion, *big.Int, *Response) { + panic("implement me") } -func (data CreateMultisigData) Gas() int64 { - return commissions.CreateMultisig -} - -func (data CreateMultisigData) Run(sender types.Address, tx *Transaction, context *state.StateDB, isCheck bool, rewardPool *big.Int, currentBlock int64) Response { +func (data CreateMultisigData) BasicCheck(tx *Transaction, context *state.StateDB) *Response { if !context.CoinExists(tx.GasCoin) { - return Response{ + return &Response{ Code: code.CoinNotExists, Log: fmt.Sprintf("Coin %s not exists", tx.GasCoin)} } if len(data.Weights) > 32 { - return Response{ + return &Response{ Code: code.TooLargeOwnersList, Log: fmt.Sprintf("Owners list is limited to 32 items")} } - commissionInBaseCoin := big.NewInt(0).Mul(tx.GasPrice, big.NewInt(tx.Gas())) - commissionInBaseCoin.Mul(commissionInBaseCoin, CommissionMultiplier) + if len(data.Addresses) != len(data.Weights) { + return &Response{ + Code: code.IncorrectWeights, + Log: fmt.Sprintf("Incorrect multisig weights")} + } + + return nil +} + +func (data CreateMultisigData) String() string { + return fmt.Sprintf("CREATE MULTISIG") +} + +func (data CreateMultisigData) Gas() int64 { + return commissions.CreateMultisig +} + +func (data CreateMultisigData) Run(tx *Transaction, context *state.StateDB, isCheck bool, rewardPool *big.Int, currentBlock int64) Response { + sender, _ := tx.Sender() + + commissionInBaseCoin := tx.CommissionInBaseCoin() commission := big.NewInt(0).Set(commissionInBaseCoin) if !tx.GasCoin.IsBaseCoin() { @@ -66,12 +68,6 @@ func (data CreateMultisigData) Run(sender types.Address, tx *Transaction, contex } commission = formula.CalculateSaleAmount(coin.Volume(), coin.ReserveBalance(), coin.Data().Crr, commissionInBaseCoin) - - if commission == nil { - return Response{ - Code: 999, - Log: "Unknown error"} - } } if context.GetBalance(sender, tx.GasCoin).Cmp(commission) < 0 { @@ -80,12 +76,6 @@ func (data CreateMultisigData) Run(sender types.Address, tx *Transaction, contex Log: fmt.Sprintf("Insufficient funds for sender account: %s. Wanted %s %s", sender.String(), commission, tx.GasCoin)} } - if len(data.Addresses) != len(data.Weights) { - return Response{ - Code: code.IncorrectWeights, - Log: fmt.Sprintf("Incorrect multisig weights")} - } - msigAddress := (&state.Multisig{ Weights: data.Weights, Threshold: data.Threshold, From 6ad8423d4fa3a66b1ecf5e48daa6d1f86a92b6cb Mon Sep 17 00:00:00 2001 From: Daniil Lashin Date: Mon, 3 Dec 2018 13:17:06 +0300 Subject: [PATCH 15/49] Refactor Multisend tx --- core/transaction/create_multisig.go | 5 ++ core/transaction/multisend.go | 75 ++++++++++++----------------- 2 files changed, 37 insertions(+), 43 deletions(-) diff --git a/core/transaction/create_multisig.go b/core/transaction/create_multisig.go index ef69a6bbf..33a981dad 100644 --- a/core/transaction/create_multisig.go +++ b/core/transaction/create_multisig.go @@ -55,6 +55,11 @@ func (data CreateMultisigData) Gas() int64 { func (data CreateMultisigData) Run(tx *Transaction, context *state.StateDB, isCheck bool, rewardPool *big.Int, currentBlock int64) Response { sender, _ := tx.Sender() + response := data.BasicCheck(tx, context) + if response != nil { + return *response + } + commissionInBaseCoin := tx.CommissionInBaseCoin() commission := big.NewInt(0).Set(commissionInBaseCoin) diff --git a/core/transaction/multisend.go b/core/transaction/multisend.go index 5d298e8ef..391a3c6f0 100644 --- a/core/transaction/multisend.go +++ b/core/transaction/multisend.go @@ -2,7 +2,6 @@ package transaction import ( "encoding/hex" - "encoding/json" "fmt" "github.com/MinterTeam/minter-go-node/core/code" "github.com/MinterTeam/minter-go-node/core/commissions" @@ -20,59 +19,55 @@ type MultisendData struct { List []MultisendDataItem } -type MultisendDataItem struct { - Coin types.CoinSymbol - To types.Address - Value *big.Int +func (data MultisendData) TotalSpend(tx *Transaction, context *state.StateDB) (TotalSpends, []Conversion, *big.Int, *Response) { + panic("implement me") } -func (data MultisendData) MarshalJSON() ([]byte, error) { - var list []interface{} - - for _, item := range data.List { - list = append(list, struct { - Coin types.CoinSymbol - To types.Address - Value string - }{ - Coin: item.Coin, - To: item.To, - Value: item.Value.String(), - }) - } - - return json.Marshal(list) -} - -func (data MultisendData) String() string { - return fmt.Sprintf("MULTISEND") -} - -func (data MultisendData) Gas() int64 { - return commissions.SendTx + ((int64(len(data.List)) - 1) * commissions.MultisendDelta) -} - -func (data MultisendData) Run(sender types.Address, tx *Transaction, context *state.StateDB, isCheck bool, rewardPool *big.Int, currentBlock int64) Response { +func (data MultisendData) BasicCheck(tx *Transaction, context *state.StateDB) *Response { if len(data.List) < 1 || len(data.List) > 100 { - return Response{ + return &Response{ Code: code.InvalidMultisendData, Log: "List length must be between 1 and 100"} } if err := checkCoins(context, data.List); err != nil { - return Response{ + return &Response{ Code: code.CoinNotExists, Log: err.Error()} } if !context.CoinExists(tx.GasCoin) { - return Response{ + return &Response{ Code: code.CoinNotExists, Log: fmt.Sprintf("Coin %s not exists", tx.GasCoin)} } - commissionInBaseCoin := big.NewInt(0).Mul(tx.GasPrice, big.NewInt(tx.Gas())) - commissionInBaseCoin.Mul(commissionInBaseCoin, CommissionMultiplier) + return nil +} + +type MultisendDataItem struct { + Coin types.CoinSymbol + To types.Address + Value *big.Int +} + +func (data MultisendData) String() string { + return fmt.Sprintf("MULTISEND") +} + +func (data MultisendData) Gas() int64 { + return commissions.SendTx + ((int64(len(data.List)) - 1) * commissions.MultisendDelta) +} + +func (data MultisendData) Run(tx *Transaction, context *state.StateDB, isCheck bool, rewardPool *big.Int, currentBlock int64) Response { + sender, _ := tx.Sender() + + response := data.BasicCheck(tx, context) + if response != nil { + return *response + } + + commissionInBaseCoin := tx.CommissionInBaseCoin() commission := big.NewInt(0).Set(commissionInBaseCoin) if !tx.GasCoin.IsBaseCoin() { @@ -85,12 +80,6 @@ func (data MultisendData) Run(sender types.Address, tx *Transaction, context *st } commission = formula.CalculateSaleAmount(coin.Volume(), coin.ReserveBalance(), coin.Data().Crr, commissionInBaseCoin) - - if commission == nil { - return Response{ - Code: 999, - Log: "Unknown error"} - } } if err := checkBalances(context, sender, data.List, commission, tx.GasCoin); err != nil { From cd2c009d9f8625146bd27a9ea1031458765ac180 Mon Sep 17 00:00:00 2001 From: Daniil Lashin Date: Mon, 3 Dec 2018 13:27:20 +0300 Subject: [PATCH 16/49] Fix json representation of transactions --- core/transaction/buy_coin.go | 6 +++--- core/transaction/buy_coin_test.go | 13 +++++++++++-- core/transaction/create_coin.go | 10 +++++----- core/transaction/create_multisig.go | 6 +++--- core/transaction/declare_candidacy.go | 10 +++++----- core/transaction/delegate.go | 6 +++--- core/transaction/executor.go | 2 +- core/transaction/multisend.go | 8 ++++---- core/transaction/redeem_check.go | 4 ++-- core/transaction/sell_all_coin.go | 4 ++-- core/transaction/sell_coin.go | 8 ++++---- core/transaction/send.go | 6 +++--- core/transaction/switch_candidate_status.go | 4 ++-- core/transaction/unbond.go | 6 +++--- 14 files changed, 51 insertions(+), 42 deletions(-) diff --git a/core/transaction/buy_coin.go b/core/transaction/buy_coin.go index 9b24914d6..dad513a90 100644 --- a/core/transaction/buy_coin.go +++ b/core/transaction/buy_coin.go @@ -13,9 +13,9 @@ import ( ) type BuyCoinData struct { - CoinToBuy types.CoinSymbol - ValueToBuy *big.Int - CoinToSell types.CoinSymbol + CoinToBuy types.CoinSymbol `json:"coin_to_buy"` + ValueToBuy *big.Int `json:"value_to_buy"` + CoinToSell types.CoinSymbol `json:"coin_to_sell"` } func (data BuyCoinData) String() string { diff --git a/core/transaction/buy_coin_test.go b/core/transaction/buy_coin_test.go index 234db5f33..ba91c56b7 100644 --- a/core/transaction/buy_coin_test.go +++ b/core/transaction/buy_coin_test.go @@ -2,18 +2,27 @@ package transaction import ( "bytes" - "encoding/json" "github.com/MinterTeam/minter-go-node/core/code" "github.com/MinterTeam/minter-go-node/core/state" "github.com/MinterTeam/minter-go-node/core/types" "github.com/MinterTeam/minter-go-node/crypto" "github.com/MinterTeam/minter-go-node/helpers" "github.com/MinterTeam/minter-go-node/rlp" + "github.com/tendermint/go-amino" + "github.com/tendermint/tendermint/crypto/encoding/amino" "github.com/tendermint/tendermint/libs/db" "math/big" "testing" ) +var ( + cdc = amino.NewCodec() +) + +func init() { + cryptoAmino.RegisterAmino(cdc) +} + func getState() *state.StateDB { s, err := state.New(0, db.NewMemDB()) @@ -380,7 +389,7 @@ func TestBuyCoinTxJSON(t *testing.T) { CoinToSell: getTestCoinSymbol(), } - result, err := json.Marshal(buyCoinData) + result, err := cdc.MarshalJSON(buyCoinData) if err != nil { t.Fatalf("Error: %s", err.Error()) diff --git a/core/transaction/create_coin.go b/core/transaction/create_coin.go index ca1537f28..15fa70873 100644 --- a/core/transaction/create_coin.go +++ b/core/transaction/create_coin.go @@ -17,11 +17,11 @@ const maxCoinNameBytes = 64 const allowedCoinSymbols = "^[A-Z0-9]{3,10}$" type CreateCoinData struct { - Name string - Symbol types.CoinSymbol - InitialAmount *big.Int - InitialReserve *big.Int - ConstantReserveRatio uint + Name string `json:"name"` + Symbol types.CoinSymbol `json:"symbol"` + InitialAmount *big.Int `json:"initial_amount"` + InitialReserve *big.Int `json:"initial_reserve"` + ConstantReserveRatio uint `json:"constant_reserve_ratio"` } func (data CreateCoinData) TotalSpend(tx *Transaction, context *state.StateDB) (TotalSpends, []Conversion, *big.Int, *Response) { diff --git a/core/transaction/create_multisig.go b/core/transaction/create_multisig.go index 33a981dad..82786108b 100644 --- a/core/transaction/create_multisig.go +++ b/core/transaction/create_multisig.go @@ -13,9 +13,9 @@ import ( ) type CreateMultisigData struct { - Threshold uint - Weights []uint - Addresses []types.Address + Threshold uint `json:"threshold"` + Weights []uint `json:"weights"` + Addresses []types.Address `json:"addresses"` } func (data CreateMultisigData) TotalSpend(tx *Transaction, context *state.StateDB) (TotalSpends, []Conversion, *big.Int, *Response) { diff --git a/core/transaction/declare_candidacy.go b/core/transaction/declare_candidacy.go index 985b1a783..d34d97ade 100644 --- a/core/transaction/declare_candidacy.go +++ b/core/transaction/declare_candidacy.go @@ -16,11 +16,11 @@ const minCommission = 0 const maxCommission = 100 type DeclareCandidacyData struct { - Address types.Address - PubKey []byte - Commission uint - Coin types.CoinSymbol - Stake *big.Int + Address types.Address `json:"address"` + PubKey []byte `json:"pub_key"` + Commission uint `json:"commission"` + Coin types.CoinSymbol `json:"coin"` + Stake *big.Int `json:"stake"` } func (data DeclareCandidacyData) TotalSpend(tx *Transaction, context *state.StateDB) (TotalSpends, []Conversion, *big.Int, *Response) { diff --git a/core/transaction/delegate.go b/core/transaction/delegate.go index 621c901d9..1ec743606 100644 --- a/core/transaction/delegate.go +++ b/core/transaction/delegate.go @@ -12,9 +12,9 @@ import ( ) type DelegateData struct { - PubKey []byte - Coin types.CoinSymbol - Stake *big.Int + PubKey []byte `json:"pub_key"` + Coin types.CoinSymbol `json:"coin"` + Stake *big.Int `json:"stake"` } func (data DelegateData) TotalSpend(tx *Transaction, context *state.StateDB) (TotalSpends, []Conversion, *big.Int, *Response) { diff --git a/core/transaction/executor.go b/core/transaction/executor.go index 83675924e..3e95dbd8b 100644 --- a/core/transaction/executor.go +++ b/core/transaction/executor.go @@ -125,5 +125,5 @@ func RunTx(context *state.StateDB, isCheck bool, rawTx []byte, rewardPool *big.I Log: fmt.Sprintf("Unexpected nonce. Expected: %d, got %d.", expectedNonce, tx.Nonce)} } - return tx.decodedData.Run(sender, tx, context, isCheck, rewardPool, currentBlock) + return tx.decodedData.Run(tx, context, isCheck, rewardPool, currentBlock) } diff --git a/core/transaction/multisend.go b/core/transaction/multisend.go index 391a3c6f0..517f287c3 100644 --- a/core/transaction/multisend.go +++ b/core/transaction/multisend.go @@ -16,7 +16,7 @@ import ( ) type MultisendData struct { - List []MultisendDataItem + List []MultisendDataItem `json:"list"` } func (data MultisendData) TotalSpend(tx *Transaction, context *state.StateDB) (TotalSpends, []Conversion, *big.Int, *Response) { @@ -46,9 +46,9 @@ func (data MultisendData) BasicCheck(tx *Transaction, context *state.StateDB) *R } type MultisendDataItem struct { - Coin types.CoinSymbol - To types.Address - Value *big.Int + Coin types.CoinSymbol `json:"coin"` + To types.Address `json:"to"` + Value *big.Int `json:"value"` } func (data MultisendData) String() string { diff --git a/core/transaction/redeem_check.go b/core/transaction/redeem_check.go index 47e5bc355..1d4074acd 100644 --- a/core/transaction/redeem_check.go +++ b/core/transaction/redeem_check.go @@ -18,8 +18,8 @@ import ( ) type RedeemCheckData struct { - RawCheck []byte - Proof [65]byte + RawCheck []byte `json:"raw_check"` + Proof [65]byte `json:"proof"` } func (data RedeemCheckData) TotalSpend(tx *Transaction, context *state.StateDB) (TotalSpends, []Conversion, *big.Int, *Response) { diff --git a/core/transaction/sell_all_coin.go b/core/transaction/sell_all_coin.go index 43159b2c2..115300fa2 100644 --- a/core/transaction/sell_all_coin.go +++ b/core/transaction/sell_all_coin.go @@ -13,8 +13,8 @@ import ( ) type SellAllCoinData struct { - CoinToSell types.CoinSymbol - CoinToBuy types.CoinSymbol + CoinToSell types.CoinSymbol `json:"coin_to_sell"` + CoinToBuy types.CoinSymbol `json:"coin_to_buy"` } func (data SellAllCoinData) TotalSpend(tx *Transaction, context *state.StateDB) (TotalSpends, []Conversion, *big.Int, *Response) { diff --git a/core/transaction/sell_coin.go b/core/transaction/sell_coin.go index 2fb38fe2c..fb3bb1860 100644 --- a/core/transaction/sell_coin.go +++ b/core/transaction/sell_coin.go @@ -13,9 +13,9 @@ import ( ) type SellCoinData struct { - CoinToSell types.CoinSymbol - ValueToSell *big.Int - CoinToBuy types.CoinSymbol + CoinToSell types.CoinSymbol `json:"coin_to_sell"` + ValueToSell *big.Int `json:"value_to_sell"` + CoinToBuy types.CoinSymbol `json:"coin_to_buy"` } func (data SellCoinData) TotalSpend(tx *Transaction, context *state.StateDB) (TotalSpends, []Conversion, *big.Int, *Response) { @@ -29,8 +29,8 @@ func (data SellCoinData) TotalSpend(tx *Transaction, context *state.StateDB) (To if data.CoinToSell.IsBaseCoin() { coin := context.GetStateCoin(data.CoinToBuy).Data() - value = formula.CalculatePurchaseReturn(coin.Volume, coin.ReserveBalance, coin.Crr, data.ValueToSell) + total.Add(data.CoinToSell, value) conversions = append(conversions, Conversion{ FromCoin: data.CoinToSell, diff --git a/core/transaction/send.go b/core/transaction/send.go index 95ee0450e..33ace4088 100644 --- a/core/transaction/send.go +++ b/core/transaction/send.go @@ -13,9 +13,9 @@ import ( ) type SendData struct { - Coin types.CoinSymbol - To types.Address - Value *big.Int + Coin types.CoinSymbol `json:"coin"` + To types.Address `json:"to"` + Value *big.Int `json:"value"` } func (data SendData) TotalSpend(tx *Transaction, context *state.StateDB) (TotalSpends, []Conversion, *big.Int, *Response) { diff --git a/core/transaction/switch_candidate_status.go b/core/transaction/switch_candidate_status.go index 86c811356..cea139ddb 100644 --- a/core/transaction/switch_candidate_status.go +++ b/core/transaction/switch_candidate_status.go @@ -12,7 +12,7 @@ import ( ) type SetCandidateOnData struct { - PubKey []byte + PubKey []byte `json:"pub_key"` } func (data SetCandidateOnData) TotalSpend(tx *Transaction, context *state.StateDB) (TotalSpends, []Conversion, *big.Int, *Response) { @@ -97,7 +97,7 @@ func (data SetCandidateOnData) Run(tx *Transaction, context *state.StateDB, isCh } type SetCandidateOffData struct { - PubKey []byte + PubKey []byte `json:"pub_key"` } func (data SetCandidateOffData) TotalSpend(tx *Transaction, context *state.StateDB) (TotalSpends, []Conversion, *big.Int, *Response) { diff --git a/core/transaction/unbond.go b/core/transaction/unbond.go index 0a7604250..593a3a264 100644 --- a/core/transaction/unbond.go +++ b/core/transaction/unbond.go @@ -14,9 +14,9 @@ import ( const unbondPeriod = 720 // in mainnet will be 518400 (30 days) type UnbondData struct { - PubKey []byte - Coin types.CoinSymbol - Value *big.Int + PubKey []byte `json:"pub_key"` + Coin types.CoinSymbol `json:"coin"` + Value *big.Int `json:"value"` } func (data UnbondData) TotalSpend(tx *Transaction, context *state.StateDB) (TotalSpends, []Conversion, *big.Int, *Response) { From 7a799b7b052b1e271cbaafd71605667ae9986163 Mon Sep 17 00:00:00 2001 From: Daniil Lashin Date: Mon, 3 Dec 2018 13:58:46 +0300 Subject: [PATCH 17/49] Small fixes --- core/transaction/sell_coin.go | 40 ++++------------------------------- core/transaction/send.go | 1 + 2 files changed, 5 insertions(+), 36 deletions(-) diff --git a/core/transaction/sell_coin.go b/core/transaction/sell_coin.go index fb3bb1860..f97c4ca25 100644 --- a/core/transaction/sell_coin.go +++ b/core/transaction/sell_coin.go @@ -30,8 +30,8 @@ func (data SellCoinData) TotalSpend(tx *Transaction, context *state.StateDB) (To if data.CoinToSell.IsBaseCoin() { coin := context.GetStateCoin(data.CoinToBuy).Data() value = formula.CalculatePurchaseReturn(coin.Volume, coin.ReserveBalance, coin.Crr, data.ValueToSell) - - total.Add(data.CoinToSell, value) + + total.Add(data.CoinToSell, data.ValueToSell) conversions = append(conversions, Conversion{ FromCoin: data.CoinToSell, ToCoin: data.CoinToBuy, @@ -59,7 +59,7 @@ func (data SellCoinData) TotalSpend(tx *Transaction, context *state.StateDB) (To rValue.Add(rValue, commissionInBaseCoin) } - total.Add(data.CoinToSell, rValue) + total.Add(data.CoinToSell, valueToSell) conversions = append(conversions, Conversion{ FromCoin: data.CoinToSell, FromAmount: valueToSell, @@ -90,7 +90,7 @@ func (data SellCoinData) TotalSpend(tx *Transaction, context *state.StateDB) (To value = formula.CalculatePurchaseReturn(coinTo.Volume, coinTo.ReserveBalance, coinTo.Crr, basecoinValue) - total.Add(data.CoinToSell, value) + total.Add(data.CoinToSell, valueToSell) conversions = append(conversions, Conversion{ FromCoin: data.CoinToSell, @@ -177,38 +177,6 @@ func (data SellCoinData) Run(tx *Transaction, context *state.StateDB, isCheck bo return *response } - if data.CoinToSell.IsBaseCoin() { - coin := context.GetStateCoin(data.CoinToBuy).Data() - value = formula.CalculatePurchaseReturn(coin.Volume, coin.ReserveBalance, coin.Crr, data.ValueToSell) - - if !isCheck { - context.AddCoinVolume(data.CoinToBuy, value) - context.AddCoinReserve(data.CoinToBuy, data.ValueToSell) - } - } else if data.CoinToBuy.IsBaseCoin() { - coin := context.GetStateCoin(data.CoinToSell).Data() - value = formula.CalculateSaleReturn(coin.Volume, coin.ReserveBalance, coin.Crr, data.ValueToSell) - - if !isCheck { - context.SubCoinVolume(data.CoinToSell, data.ValueToSell) - context.SubCoinReserve(data.CoinToSell, value) - } - } else { - coinFrom := context.GetStateCoin(data.CoinToSell).Data() - coinTo := context.GetStateCoin(data.CoinToBuy).Data() - - basecoinValue := formula.CalculateSaleReturn(coinFrom.Volume, coinFrom.ReserveBalance, coinFrom.Crr, data.ValueToSell) - value = formula.CalculatePurchaseReturn(coinTo.Volume, coinTo.ReserveBalance, coinTo.Crr, basecoinValue) - - if !isCheck { - context.AddCoinVolume(data.CoinToBuy, value) - context.SubCoinVolume(data.CoinToSell, data.ValueToSell) - - context.AddCoinReserve(data.CoinToBuy, basecoinValue) - context.SubCoinReserve(data.CoinToSell, basecoinValue) - } - } - for _, ts := range totalSpends { if context.GetBalance(sender, ts.Coin).Cmp(ts.Value) < 0 { return Response{ diff --git a/core/transaction/send.go b/core/transaction/send.go index 33ace4088..bfb88fa6b 100644 --- a/core/transaction/send.go +++ b/core/transaction/send.go @@ -46,6 +46,7 @@ func (data SendData) TotalSpend(tx *Transaction, context *state.StateDB) (TotalS } total.Add(tx.GasCoin, commission) + total.Add(data.Coin, data.Value) return total, conversions, nil, nil } From 9c94f69df271abff3dcfdfc84629dc4468d28a63 Mon Sep 17 00:00:00 2001 From: Daniil Lashin Date: Mon, 3 Dec 2018 15:05:56 +0300 Subject: [PATCH 18/49] Bump version and update changelog --- CHANGELOG.md | 5 +++-- docker-compose.yml | 2 +- version/version.go | 6 +++--- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e9e08944b..10dbf568e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,12 @@ # Changelog -## 0.7.7 -*Nov 28th, 2018* +## 0.8.0 +*Dec 3rd, 2018* BREAKING CHANGES - [api] Switch to RPC protocol +- [core] Fix issue with incorrect coin conversion IMPROVEMENT diff --git a/docker-compose.yml b/docker-compose.yml index eee078e9c..83f0227d3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,7 +1,7 @@ version: "3.4" services: minter: - image: minterteam/minter:0.7.7 + image: minterteam/minter:0.8.0 volumes: - ~/.minter:/minter ports: diff --git a/version/version.go b/version/version.go index f992e9c68..a65cab728 100755 --- a/version/version.go +++ b/version/version.go @@ -3,13 +3,13 @@ package version // Version components const ( Maj = "0" - Min = "7" - Fix = "7" + Min = "8" + Fix = "0" ) var ( // Must be a string because scripts like dist.sh read this file. - Version = "0.7.7" + Version = "0.8.0" // GitCommit is the current HEAD set using ldflags. GitCommit string From c80f521bf32f04333cc4e62ad5638c72c40614a5 Mon Sep 17 00:00:00 2001 From: Daniil Lashin Date: Mon, 3 Dec 2018 15:34:59 +0300 Subject: [PATCH 19/49] Update genesis --- genesis/genesis.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/genesis/genesis.go b/genesis/genesis.go index 41a48abad..9f9bddc7c 100644 --- a/genesis/genesis.go +++ b/genesis/genesis.go @@ -12,8 +12,8 @@ import ( ) var ( - Network = "minter-test-network-26" - genesisTime = time.Date(2018, 11, 16, 9, 0, 0, 0, time.UTC) + Network = "minter-test-network-27" + genesisTime = time.Date(2018, 12, 10, 9, 0, 0, 0, time.UTC) ) func GetTestnetGenesis() (*tmtypes.GenesisDoc, error) { From 6299fb2759f76b55f17fd0ea397d4f2cdc2e79ae Mon Sep 17 00:00:00 2001 From: Daniil Lashin Date: Mon, 3 Dec 2018 16:51:46 +0300 Subject: [PATCH 20/49] Update API docs --- docs/api.rst | 204 +++++++++++++++++++-------------------------------- 1 file changed, 75 insertions(+), 129 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index 8f2c58869..12bc1626a 100755 --- a/docs/api.rst +++ b/docs/api.rst @@ -15,73 +15,56 @@ normal mode. .. code-block:: bash - curl -s 'localhost:8841/api/status' + curl -s 'localhost:8841/status' .. code-block:: json { - "code": 0, + "jsonrpc": "2.0", + "id": "", "result": { - "version": "0.2.5", - "latest_block_hash": "0CC015EA926173130C793BBE6E38145BF379CF6A", - "latest_app_hash": "1FB9B53F32298759D936E4A10A866E7AFB930EA4D7CC7184EC992F2320592E81", - "latest_block_height": 82541, - "latest_block_time": "2018-08-28T18:26:47.112704193+03:00", + "version": "0.8.0", + "latest_block_hash": "171F3A749F85425147986DD90EA0C397440B6A3C1FEF8F30E5E5F729DA174CC2", + "latest_app_hash": "55E75C9860E56AF3DEB8DD55741185F658569AB43C084436DDDB69CBFB06CC63", + "latest_block_height": "4", + "latest_block_time": "2018-12-03T13:18:42.50969Z", "tm_status": { "node_info": { - "id": "62a5d75ef3f48dcf62aad263a170b9c82eb3f2b8", - "listen_addr": "192.168.1.100:26656", - "network": "minter-test-network-19", - "version": "0.23.0", + "protocol_version": { + "p2p": "4", + "block": "7", + "app": "0" + }, + "id": "9d5eb9f8fb7ada3ff6228841c3500f39e3121901", + "listen_addr": "tcp://0.0.0.0:26656", + "network": "minter-test-network-27-local", + "version": "0.26.4", "channels": "4020212223303800", - "moniker": "MinterNode", - "other": [ - "amino_version=0.10.1", - "p2p_version=0.5.0", - "consensus_version=v1/0.2.2", - "rpc_version=0.7.0/3", - "tx_index=on", - "rpc_addr=tcp://0.0.0.0:26657" - ] + "moniker": "MacBook-Pro-Daniil-2.local", + "other": { + "tx_index": "on", + "rpc_address": "tcp://0.0.0.0:26657" + } }, "sync_info": { - "latest_block_hash": "0CC015EA926173130C793BBE6E38145BF379CF6A", - "latest_app_hash": "1FB9B53F32298759D936E4A10A866E7AFB930EA4D7CC7184EC992F2320592E81", - "latest_block_height": "82541", - "latest_block_time": "2018-08-28T15:26:47.112704193Z", - "catching_up": true + "latest_block_hash": "171F3A749F85425147986DD90EA0C397440B6A3C1FEF8F30E5E5F729DA174CC2", + "latest_app_hash": "55E75C9860E56AF3DEB8DD55741185F658569AB43C084436DDDB69CBFB06CC63", + "latest_block_height": "4", + "latest_block_time": "2018-12-03T13:18:42.50969Z", + "catching_up": false }, "validator_info": { - "address": "BCFB297FD1EE0458E1DBDA8EBAE2C599CD0A5984", + "address": "AB15A084DD592699812E9B22385C1959E7AEFFB8", "pub_key": { "type": "tendermint/PubKeyEd25519", - "value": "G2lZ+lJWW/kQvhOOI6CHVBHSEgjYq9awDgdlErLeVAE=" + "value": "4LpQ40aLB/u8EnhAlT649P5X1ugWLfk7rv159dW8K5c=" }, - "voting_power": "0" + "voting_power": "100000000" } } } } -Volume of Base Coin in Blockchain -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -This endpoint shows amount of base coin (BIP or MNT) existing in the network. It counts block rewards, premine and -relayed rewards. - -.. code-block:: bash - - curl -s 'localhost:8841/api/bipVolume?height={height}' - -.. code-block:: json - - { - "code":0, - "result":{ - "volume":"20000222000000000000000000" - } - } - Candidate ^^^^^^^^^ @@ -105,38 +88,32 @@ found. .. code-block:: bash - curl -s 'localhost:8841/api/candidate/{public_key}' + curl -s 'localhost:8841/candidate?pubkey={public_key}' .. code-block:: json { - "code": 0, + "jsonrpc": "2.0", + "id": "", "result": { - "candidate": { - "candidate_address": "Mxee81347211c72524338f9680072af90744333146", - "total_stake": "5000001000000000000000000", - "pub_key": "Mp738da41ba6a7b7d69b7294afa158b89c5a1b410cbf0c2443c85c5fe24ad1dd1c", - "commission": 100, - "stakes": [ - { - "owner": "Mxee81347211c72524338f9680072af90744333146", - "coin": "MNT", - "value": "5000000000000000000000000", - "bip_value": "5000000000000000000000000" - }, - { - "owner": "Mx4f3385615a4abb104d6eda88591fa07c112cbdbf", - "coin": "MNT", - "value": "1000000000000000000", - "bip_value": "1000000000000000000" - } - ], - "created_at_block": 165, - "status": 2 - } + "candidate_address": "Mxee81347211c72524338f9680072af90744333146", + "total_stake": 0, + "pub_key": "Mpe0ba50e3468b07fbbc127840953eb8f4fe57d6e8162df93baefd79f5d5bc2b97", + "commission": "100", + "stakes": [ + { + "owner": "Mxee81347211c72524338f9680072af90744333146", + "coin": "MNT", + "value": "1000000000000000000000000", + "bip_value": "1000000000000000000000000" + } + ], + "created_at_block": "1", + "status": 2 } } + Validators ^^^^^^^^^^ @@ -144,34 +121,23 @@ Returns list of active validators. .. code-block:: bash - curl -s 'localhost:8841/api/validators' + curl -s 'localhost:8841/validators' .. code-block:: json { - "code": 0, + "jsonrpc": "2.0", + "id": "", "result": [ { - "accumulated_reward": "652930049792069211272", - "absent_times": 0, + "accumulated_reward": 2331000000000000000000, + "absent_times": "0", "candidate": { "candidate_address": "Mxee81347211c72524338f9680072af90744333146", - "total_stake": "5000001000000000000000000", - "pub_key": "Mp738da41ba6a7b7d69b7294afa158b89c5a1b410cbf0c2443c85c5fe24ad1dd1c", - "commission": 100, - "created_at_block": 165, - "status": 2 - } - }, - { - "accumulated_reward": "652929919206085370058", - "absent_times": 0, - "candidate": { - "candidate_address": "Mxee81347211c72524338f9680072af90744333146", - "total_stake": "5000000000000000000000000", - "pub_key": "Mp6f16c1ff21a6fb946aaed0f4c1fcca272b72fd904988f91d3883282b8ae31ba2", - "commission": 100, - "created_at_block": 174, + "total_stake": 0, + "pub_key": "Mpe0ba50e3468b07fbbc127840953eb8f4fe57d6e8162df93baefd79f5d5bc2b97", + "commission": "100", + "created_at_block": "1", "status": 2 } } @@ -179,53 +145,33 @@ Returns list of active validators. } -Balance + +Address ^^^^^^^ -Returns balance of an account. +Returns the balance of given account and the number of outgoing transaction. .. code-block:: bash - curl -s 'localhost:8841/api/balance/{address}' + curl -s 'localhost:8841/address?address={address}' .. code-block:: json { - "code": 0, + "jsonrpc": "2.0", + "id": "", "result": { "balance": { - "MINTERONE": "2000000000000000000", - "MNT": "97924621949581028367025445", - "SHSCOIN": "201502537939970000000000", - "TESTCOIN": "1000000000000000000000" - } + "MNT": "100010489500000000000000000" + }, + "transaction_count": "0" } } -**Result**: Map of balances. CoinSymbol => Balance (in pips). - -Transaction count -^^^^^^^^^^^^^^^^^ - -Returns count of outgoing transactions from given account. This should be used for calculating nonce for the new -transaction. - -.. code-block:: bash - - curl -s 'localhost:8841/api/transactionCount/{address}' - -.. code-block:: json - - { - "code": 0, - "result": { - "count": 59 - } - } - -**Result**: Count of transactions sent from given account. +**Result->balance**: Map of balances. CoinSymbol => Balance (in pips). +**Result->transaction_count**: Count of transactions sent from the account. Send transaction ^^^^^^^^^^^^^^^^ @@ -234,7 +180,7 @@ Sends transaction to the Minter Network. .. code-block:: bash - curl -X POST --data '{"transaction":"..."}' -s 'localhost:8841/api/sendTransaction' + curl -s 'localhost:8841/send_transaction?tx={transaction}' .. code-block:: json @@ -252,7 +198,7 @@ Transaction .. code-block:: bash - curl -s 'localhost:8841/api/transaction/{hash}' + curl -s 'localhost:8841/transaction?hash={hash}' .. code-block:: json @@ -291,7 +237,7 @@ Returns block data at given height. .. code-block:: bash - curl -s 'localhost:8841/api/block/{height}' + curl -s 'localhost:8841/block/{height}' .. code-block:: json @@ -425,7 +371,7 @@ Returns information about coin. .. code-block:: bash - curl -s 'localhost:8841/api/coinInfo/{symbol}' + curl -s 'localhost:8841/coinInfo/{symbol}' .. code-block:: json @@ -456,7 +402,7 @@ Return estimate of sell coin transaction .. code-block:: bash - curl -s 'localhost:8841/api/estimateCoinSell?coin_to_sell=MNT&value_to_sell=1000000000000000000&coin_to_buy=BLTCOIN' + curl -s 'localhost:8841/estimateCoinSell?coin_to_sell=MNT&value_to_sell=1000000000000000000&coin_to_buy=BLTCOIN' Request params: - **coin_to_sell** – coin to give @@ -483,7 +429,7 @@ Return estimate of buy coin transaction .. code-block:: bash - curl -s 'localhost:8841/api/estimateCoinBuy?coin_to_sell=MNT&value_to_buy=1000000000000000000&coin_to_buy=BLTCOIN' + curl -s 'localhost:8841/estimateCoinBuy?coin_to_sell=MNT&value_to_buy=1000000000000000000&coin_to_buy=BLTCOIN' Request params: - **coin_to_sell** – coin to give @@ -509,7 +455,7 @@ Return estimate of buy coin transaction .. code-block:: bash - curl -s 'localhost:8841/api/estimateTxCommission?tx={transaction}' + curl -s 'localhost:8841/estimateTxCommission?tx={transaction}' .. code-block:: json From d8b1ace936c59259cc7e76b1835694bdd45b654a Mon Sep 17 00:00:00 2001 From: Daniil Lashin Date: Mon, 3 Dec 2018 17:32:46 +0300 Subject: [PATCH 21/49] Refactor RPC --- Gopkg.lock | 58 +- Gopkg.toml | 4 + api/api.go | 29 +- eventsdb/amino.go | 2 +- eventsdb/eventsdb.go | 2 +- rpc/lib/doc.go | 85 +++ rpc/lib/rpc_test.go | 375 +++++++++++++ rpc/lib/server/handlers.go | 838 +++++++++++++++++++++++++++++ rpc/lib/server/handlers_test.go | 207 +++++++ rpc/lib/server/http_params.go | 91 ++++ rpc/lib/server/http_server.go | 201 +++++++ rpc/lib/server/http_server_test.go | 79 +++ rpc/lib/server/parse_test.go | 221 ++++++++ rpc/lib/types/types.go | 272 ++++++++++ rpc/lib/types/types_test.go | 84 +++ rpc/test/helpers.go | 138 +++++ 16 files changed, 2663 insertions(+), 23 deletions(-) create mode 100644 rpc/lib/doc.go create mode 100644 rpc/lib/rpc_test.go create mode 100644 rpc/lib/server/handlers.go create mode 100644 rpc/lib/server/handlers_test.go create mode 100644 rpc/lib/server/http_params.go create mode 100644 rpc/lib/server/http_server.go create mode 100644 rpc/lib/server/http_server_test.go create mode 100644 rpc/lib/server/parse_test.go create mode 100644 rpc/lib/types/types.go create mode 100644 rpc/lib/types/types_test.go create mode 100644 rpc/test/helpers.go diff --git a/Gopkg.lock b/Gopkg.lock index 637f3e9a3..0b1533342 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -1,6 +1,14 @@ # This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. +[[projects]] + digest = "1:949805904cdeb760694dea738d9b6a2b9cb2bea7ce6f3be1d281153b59901a8c" + name = "github.com/MinterTeam/go-amino" + packages = ["."] + pruneopts = "UT" + revision = "fd167714c81364d0eb8ee5f02916503454026e92" + version = "v0.14.1-m" + [[projects]] branch = "master" digest = "1:d6afaeed1502aa28e80a4ed0981d570ad91b2579193404256ce672ed0a609e0d" @@ -117,22 +125,6 @@ pruneopts = "UT" revision = "2e65f85255dbc3072edf28d6b5b8efc472979f5a" -[[projects]] - digest = "1:c79fb010be38a59d657c48c6ba1d003a8aa651fa56b579d959d74573b7dff8e1" - name = "github.com/gorilla/context" - packages = ["."] - pruneopts = "UT" - revision = "08b5f424b9271eedf6f9f0ce86cb9396ed337a42" - version = "v1.1.1" - -[[projects]] - digest = "1:e73f5b0152105f18bc131fba127d9949305c8693f8a762588a82a48f61756f5f" - name = "github.com/gorilla/mux" - packages = ["."] - pruneopts = "UT" - revision = "e3702bed27f0d39777b0b37b664b6280e8ef8fbf" - version = "v1.6.2" - [[projects]] digest = "1:43dd08a10854b2056e615d1b1d22ac94559d822e1f8b6fcc92c1a1057e85188e" name = "github.com/gorilla/websocket" @@ -232,6 +224,14 @@ revision = "645ef00459ed84a119197bfb8d8205042c6df63d" version = "v0.8.0" +[[projects]] + digest = "1:0028cb19b2e4c3112225cd871870f2d9cf49b9b4276531f03438a88e94be86fe" + name = "github.com/pmezard/go-difflib" + packages = ["difflib"] + pruneopts = "UT" + revision = "792786c7400a136282c1664665ae0a8db921c6c2" + version = "v1.0.0" + [[projects]] digest = "1:26663fafdea73a38075b07e8e9d82fc0056379d2be8bb4e13899e8fda7c7dd23" name = "github.com/prometheus/client_golang" @@ -335,6 +335,17 @@ revision = "2c12c60302a5a0e62ee102ca9bc996277c2f64f5" version = "v1.2.1" +[[projects]] + digest = "1:c40d65817cdd41fac9aa7af8bed56927bb2d6d47e4fea566a74880f5c2b1c41e" + name = "github.com/stretchr/testify" + packages = [ + "assert", + "require", + ] + pruneopts = "UT" + revision = "f35b8ab0b5a2cef36673838d662e249dd9c94686" + version = "v1.2.2" + [[projects]] branch = "master" digest = "1:9ff9e1808adfc43f788f1c1e9fd2660c285b522243da985a4c043ec6f2a2d736" @@ -562,29 +573,42 @@ analyzer-name = "dep" analyzer-version = 1 input-imports = [ + "github.com/MinterTeam/go-amino", "github.com/btcsuite/btcd/btcec", "github.com/danil-lashin/iavl", + "github.com/go-kit/kit/log/term", "github.com/gobuffalo/packr", - "github.com/gorilla/mux", + "github.com/gorilla/websocket", "github.com/pkg/errors", "github.com/rs/cors", "github.com/spf13/viper", + "github.com/stretchr/testify/assert", + "github.com/stretchr/testify/require", "github.com/tendermint/go-amino", "github.com/tendermint/tendermint/abci/types", "github.com/tendermint/tendermint/config", + "github.com/tendermint/tendermint/crypto", "github.com/tendermint/tendermint/crypto/ed25519", "github.com/tendermint/tendermint/crypto/encoding/amino", + "github.com/tendermint/tendermint/crypto/multisig", + "github.com/tendermint/tendermint/crypto/secp256k1", "github.com/tendermint/tendermint/libs/cli/flags", "github.com/tendermint/tendermint/libs/common", "github.com/tendermint/tendermint/libs/db", "github.com/tendermint/tendermint/libs/log", + "github.com/tendermint/tendermint/libs/pubsub", "github.com/tendermint/tendermint/node", "github.com/tendermint/tendermint/p2p", "github.com/tendermint/tendermint/privval", "github.com/tendermint/tendermint/proxy", "github.com/tendermint/tendermint/rpc/client", "github.com/tendermint/tendermint/rpc/core/types", + "github.com/tendermint/tendermint/rpc/grpc", + "github.com/tendermint/tendermint/rpc/lib/client", + "github.com/tendermint/tendermint/rpc/lib/server", + "github.com/tendermint/tendermint/rpc/lib/types", "github.com/tendermint/tendermint/types", + "golang.org/x/net/netutil", "gopkg.in/check.v1", ] solver-name = "gps-cdcl" diff --git a/Gopkg.toml b/Gopkg.toml index 2e11f18bb..baa0a53ee 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -49,6 +49,10 @@ name = "github.com/tendermint/tendermint" version = "=0.26.4" +[[constraint]] + name = "github.com/MinterTeam/go-amino" + version = "=v0.14.1-m" + [[constraint]] branch = "v1" name = "gopkg.in/check.v1" diff --git a/api/api.go b/api/api.go index 0894629bb..246e438b6 100644 --- a/api/api.go +++ b/api/api.go @@ -2,16 +2,19 @@ package api import ( "fmt" + "github.com/MinterTeam/go-amino" "github.com/MinterTeam/minter-go-node/config" "github.com/MinterTeam/minter-go-node/core/minter" "github.com/MinterTeam/minter-go-node/core/state" "github.com/MinterTeam/minter-go-node/eventsdb" "github.com/MinterTeam/minter-go-node/log" + "github.com/MinterTeam/minter-go-node/rpc/lib/server" "github.com/rs/cors" - "github.com/tendermint/go-amino" - "github.com/tendermint/tendermint/crypto/encoding/amino" + "github.com/tendermint/tendermint/crypto" + "github.com/tendermint/tendermint/crypto/ed25519" + "github.com/tendermint/tendermint/crypto/multisig" + "github.com/tendermint/tendermint/crypto/secp256k1" rpc "github.com/tendermint/tendermint/rpc/client" - "github.com/tendermint/tendermint/rpc/lib/server" "net/http" "net/url" "strings" @@ -26,7 +29,7 @@ var ( ) func init() { - cryptoAmino.RegisterAmino(cdc) + RegisterCryptoAmino(cdc) eventsdb.RegisterAminoEvents(cdc) } @@ -125,3 +128,21 @@ func GetStateForHeight(height int) (*state.StateDB, error) { return blockchain.CurrentState(), nil } + +// RegisterAmino registers all crypto related types in the given (amino) codec. +func RegisterCryptoAmino(cdc *amino.Codec) { + // These are all written here instead of + cdc.RegisterInterface((*crypto.PubKey)(nil), nil) + cdc.RegisterConcrete(ed25519.PubKeyEd25519{}, + ed25519.PubKeyAminoRoute, nil) + cdc.RegisterConcrete(secp256k1.PubKeySecp256k1{}, + secp256k1.PubKeyAminoRoute, nil) + cdc.RegisterConcrete(multisig.PubKeyMultisigThreshold{}, + multisig.PubKeyMultisigThresholdAminoRoute, nil) + + cdc.RegisterInterface((*crypto.PrivKey)(nil), nil) + cdc.RegisterConcrete(ed25519.PrivKeyEd25519{}, + ed25519.PrivKeyAminoRoute, nil) + cdc.RegisterConcrete(secp256k1.PrivKeySecp256k1{}, + secp256k1.PrivKeyAminoRoute, nil) +} diff --git a/eventsdb/amino.go b/eventsdb/amino.go index c443c8e63..08b9d274f 100644 --- a/eventsdb/amino.go +++ b/eventsdb/amino.go @@ -1,6 +1,6 @@ package eventsdb -import "github.com/tendermint/go-amino" +import "github.com/MinterTeam/go-amino" func RegisterAminoEvents(codec *amino.Codec) { codec.RegisterInterface((*Event)(nil), nil) diff --git a/eventsdb/eventsdb.go b/eventsdb/eventsdb.go index e5c326e80..76eed34ad 100644 --- a/eventsdb/eventsdb.go +++ b/eventsdb/eventsdb.go @@ -2,9 +2,9 @@ package eventsdb import ( "encoding/binary" + "github.com/MinterTeam/go-amino" "github.com/MinterTeam/minter-go-node/cmd/utils" "github.com/MinterTeam/minter-go-node/config" - "github.com/tendermint/go-amino" "github.com/tendermint/tendermint/libs/db" "sync" ) diff --git a/rpc/lib/doc.go b/rpc/lib/doc.go new file mode 100644 index 000000000..aa9638bfd --- /dev/null +++ b/rpc/lib/doc.go @@ -0,0 +1,85 @@ +// HTTP RPC server supporting calls via uri params, jsonrpc, and jsonrpc over websockets +// +// Client Requests +// +// Suppose we want to expose the rpc function `HelloWorld(name string, num int)`. +// +// GET (URI) +// +// As a GET request, it would have URI encoded parameters, and look like: +// +// curl 'http://localhost:8008/hello_world?name="my_world"&num=5' +// +// Note the `'` around the url, which is just so bash doesn't ignore the quotes in `"my_world"`. +// This should also work: +// +// curl http://localhost:8008/hello_world?name=\"my_world\"&num=5 +// +// A GET request to `/` returns a list of available endpoints. +// For those which take arguments, the arguments will be listed in order, with `_` where the actual value should be. +// +// POST (JSONRPC) +// +// As a POST request, we use JSONRPC. For instance, the same request would have this as the body: +// +// { +// "jsonrpc": "2.0", +// "id": "anything", +// "method": "hello_world", +// "params": { +// "name": "my_world", +// "num": 5 +// } +// } +// +// With the above saved in file `data.json`, we can make the request with +// +// curl --data @data.json http://localhost:8008 +// +// +// WebSocket (JSONRPC) +// +// All requests are exposed over websocket in the same form as the POST JSONRPC. +// Websocket connections are available at their own endpoint, typically `/websocket`, +// though this is configurable when starting the server. +// +// Server Definition +// +// Define some types and routes: +// +// type ResultStatus struct { +// Value string +// } +// +// Define some routes +// +// var Routes = map[string]*rpcserver.RPCFunc{ +// "status": rpcserver.NewRPCFunc(Status, "arg"), +// } +// +// An rpc function: +// +// func Status(v string) (*ResultStatus, error) { +// return &ResultStatus{v}, nil +// } +// +// Now start the server: +// +// mux := http.NewServeMux() +// rpcserver.RegisterRPCFuncs(mux, Routes) +// wm := rpcserver.NewWebsocketManager(Routes) +// mux.HandleFunc("/websocket", wm.WebsocketHandler) +// logger := log.NewTMLogger(log.NewSyncWriter(os.Stdout)) +// listener, err := rpc.Listen("0.0.0.0:8080", rpcserver.Config{}) +// if err != nil { panic(err) } +// go rpcserver.StartHTTPServer(listener, mux, logger) +// +// Note that unix sockets are supported as well (eg. `/path/to/socket` instead of `0.0.0.0:8008`) +// Now see all available endpoints by sending a GET request to `0.0.0.0:8008`. +// Each route is available as a GET request, as a JSONRPCv2 POST request, and via JSONRPCv2 over websockets. +// +// Examples +// +// - [Tendermint](https://github.com/tendermint/tendermint/blob/master/rpc/core/routes.go) +// - [tm-monitor](https://github.com/tendermint/tendermint/blob/master/tools/tm-monitor/rpc.go) +package rpc diff --git a/rpc/lib/rpc_test.go b/rpc/lib/rpc_test.go new file mode 100644 index 000000000..794ab462c --- /dev/null +++ b/rpc/lib/rpc_test.go @@ -0,0 +1,375 @@ +package rpc + +import ( + "bytes" + "context" + crand "crypto/rand" + "encoding/json" + "fmt" + "net/http" + "os" + "os/exec" + "testing" + "time" + + "github.com/go-kit/kit/log/term" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + amino "github.com/tendermint/go-amino" + cmn "github.com/tendermint/tendermint/libs/common" + "github.com/tendermint/tendermint/libs/log" + + client "github.com/tendermint/tendermint/rpc/lib/client" + server "github.com/tendermint/tendermint/rpc/lib/server" + types "github.com/tendermint/tendermint/rpc/lib/types" +) + +// Client and Server should work over tcp or unix sockets +const ( + tcpAddr = "tcp://0.0.0.0:47768" + + unixSocket = "/tmp/rpc_test.sock" + unixAddr = "unix://" + unixSocket + + websocketEndpoint = "/websocket/endpoint" +) + +type ResultEcho struct { + Value string `json:"value"` +} + +type ResultEchoInt struct { + Value int `json:"value"` +} + +type ResultEchoBytes struct { + Value []byte `json:"value"` +} + +type ResultEchoDataBytes struct { + Value cmn.HexBytes `json:"value"` +} + +// Define some routes +var Routes = map[string]*server.RPCFunc{ + "echo": server.NewRPCFunc(EchoResult, "arg"), + "echo_ws": server.NewWSRPCFunc(EchoWSResult, "arg"), + "echo_bytes": server.NewRPCFunc(EchoBytesResult, "arg"), + "echo_data_bytes": server.NewRPCFunc(EchoDataBytesResult, "arg"), + "echo_int": server.NewRPCFunc(EchoIntResult, "arg"), +} + +// Amino codec required to encode/decode everything above. +var RoutesCdc = amino.NewCodec() + +func EchoResult(v string) (*ResultEcho, error) { + return &ResultEcho{v}, nil +} + +func EchoWSResult(wsCtx types.WSRPCContext, v string) (*ResultEcho, error) { + return &ResultEcho{v}, nil +} + +func EchoIntResult(v int) (*ResultEchoInt, error) { + return &ResultEchoInt{v}, nil +} + +func EchoBytesResult(v []byte) (*ResultEchoBytes, error) { + return &ResultEchoBytes{v}, nil +} + +func EchoDataBytesResult(v cmn.HexBytes) (*ResultEchoDataBytes, error) { + return &ResultEchoDataBytes{v}, nil +} + +func TestMain(m *testing.M) { + setup() + code := m.Run() + os.Exit(code) +} + +var colorFn = func(keyvals ...interface{}) term.FgBgColor { + for i := 0; i < len(keyvals)-1; i += 2 { + if keyvals[i] == "socket" { + if keyvals[i+1] == "tcp" { + return term.FgBgColor{Fg: term.DarkBlue} + } else if keyvals[i+1] == "unix" { + return term.FgBgColor{Fg: term.DarkCyan} + } + } + } + return term.FgBgColor{} +} + +// launch unix and tcp servers +func setup() { + logger := log.NewTMLoggerWithColorFn(log.NewSyncWriter(os.Stdout), colorFn) + + cmd := exec.Command("rm", "-f", unixSocket) + err := cmd.Start() + if err != nil { + panic(err) + } + if err = cmd.Wait(); err != nil { + panic(err) + } + + tcpLogger := logger.With("socket", "tcp") + mux := http.NewServeMux() + server.RegisterRPCFuncs(mux, Routes, RoutesCdc, tcpLogger) + wm := server.NewWebsocketManager(Routes, RoutesCdc, server.ReadWait(5*time.Second), server.PingPeriod(1*time.Second)) + wm.SetLogger(tcpLogger) + mux.HandleFunc(websocketEndpoint, wm.WebsocketHandler) + listener1, err := server.Listen(tcpAddr, server.Config{}) + if err != nil { + panic(err) + } + go server.StartHTTPServer(listener1, mux, tcpLogger) + + unixLogger := logger.With("socket", "unix") + mux2 := http.NewServeMux() + server.RegisterRPCFuncs(mux2, Routes, RoutesCdc, unixLogger) + wm = server.NewWebsocketManager(Routes, RoutesCdc) + wm.SetLogger(unixLogger) + mux2.HandleFunc(websocketEndpoint, wm.WebsocketHandler) + listener2, err := server.Listen(unixAddr, server.Config{}) + if err != nil { + panic(err) + } + go server.StartHTTPServer(listener2, mux2, unixLogger) + + // wait for servers to start + time.Sleep(time.Second * 2) +} + +func echoViaHTTP(cl client.HTTPClient, val string) (string, error) { + params := map[string]interface{}{ + "arg": val, + } + result := new(ResultEcho) + if _, err := cl.Call("echo", params, result); err != nil { + return "", err + } + return result.Value, nil +} + +func echoIntViaHTTP(cl client.HTTPClient, val int) (int, error) { + params := map[string]interface{}{ + "arg": val, + } + result := new(ResultEchoInt) + if _, err := cl.Call("echo_int", params, result); err != nil { + return 0, err + } + return result.Value, nil +} + +func echoBytesViaHTTP(cl client.HTTPClient, bytes []byte) ([]byte, error) { + params := map[string]interface{}{ + "arg": bytes, + } + result := new(ResultEchoBytes) + if _, err := cl.Call("echo_bytes", params, result); err != nil { + return []byte{}, err + } + return result.Value, nil +} + +func echoDataBytesViaHTTP(cl client.HTTPClient, bytes cmn.HexBytes) (cmn.HexBytes, error) { + params := map[string]interface{}{ + "arg": bytes, + } + result := new(ResultEchoDataBytes) + if _, err := cl.Call("echo_data_bytes", params, result); err != nil { + return []byte{}, err + } + return result.Value, nil +} + +func testWithHTTPClient(t *testing.T, cl client.HTTPClient) { + val := "acbd" + got, err := echoViaHTTP(cl, val) + require.Nil(t, err) + assert.Equal(t, got, val) + + val2 := randBytes(t) + got2, err := echoBytesViaHTTP(cl, val2) + require.Nil(t, err) + assert.Equal(t, got2, val2) + + val3 := cmn.HexBytes(randBytes(t)) + got3, err := echoDataBytesViaHTTP(cl, val3) + require.Nil(t, err) + assert.Equal(t, got3, val3) + + val4 := cmn.RandIntn(10000) + got4, err := echoIntViaHTTP(cl, val4) + require.Nil(t, err) + assert.Equal(t, got4, val4) +} + +func echoViaWS(cl *client.WSClient, val string) (string, error) { + params := map[string]interface{}{ + "arg": val, + } + err := cl.Call(context.Background(), "echo", params) + if err != nil { + return "", err + } + + msg := <-cl.ResponsesCh + if msg.Error != nil { + return "", err + + } + result := new(ResultEcho) + err = json.Unmarshal(msg.Result, result) + if err != nil { + return "", nil + } + return result.Value, nil +} + +func echoBytesViaWS(cl *client.WSClient, bytes []byte) ([]byte, error) { + params := map[string]interface{}{ + "arg": bytes, + } + err := cl.Call(context.Background(), "echo_bytes", params) + if err != nil { + return []byte{}, err + } + + msg := <-cl.ResponsesCh + if msg.Error != nil { + return []byte{}, msg.Error + + } + result := new(ResultEchoBytes) + err = json.Unmarshal(msg.Result, result) + if err != nil { + return []byte{}, nil + } + return result.Value, nil +} + +func testWithWSClient(t *testing.T, cl *client.WSClient) { + val := "acbd" + got, err := echoViaWS(cl, val) + require.Nil(t, err) + assert.Equal(t, got, val) + + val2 := randBytes(t) + got2, err := echoBytesViaWS(cl, val2) + require.Nil(t, err) + assert.Equal(t, got2, val2) +} + +//------------- + +func TestServersAndClientsBasic(t *testing.T) { + serverAddrs := [...]string{tcpAddr, unixAddr} + for _, addr := range serverAddrs { + cl1 := client.NewURIClient(addr) + fmt.Printf("=== testing server on %s using URI client", addr) + testWithHTTPClient(t, cl1) + + cl2 := client.NewJSONRPCClient(addr) + fmt.Printf("=== testing server on %s using JSONRPC client", addr) + testWithHTTPClient(t, cl2) + + cl3 := client.NewWSClient(addr, websocketEndpoint) + cl3.SetLogger(log.TestingLogger()) + err := cl3.Start() + require.Nil(t, err) + fmt.Printf("=== testing server on %s using WS client", addr) + testWithWSClient(t, cl3) + cl3.Stop() + } +} + +func TestHexStringArg(t *testing.T) { + cl := client.NewURIClient(tcpAddr) + // should NOT be handled as hex + val := "0xabc" + got, err := echoViaHTTP(cl, val) + require.Nil(t, err) + assert.Equal(t, got, val) +} + +func TestQuotedStringArg(t *testing.T) { + cl := client.NewURIClient(tcpAddr) + // should NOT be unquoted + val := "\"abc\"" + got, err := echoViaHTTP(cl, val) + require.Nil(t, err) + assert.Equal(t, got, val) +} + +func TestWSNewWSRPCFunc(t *testing.T) { + cl := client.NewWSClient(tcpAddr, websocketEndpoint) + cl.SetLogger(log.TestingLogger()) + err := cl.Start() + require.Nil(t, err) + defer cl.Stop() + + val := "acbd" + params := map[string]interface{}{ + "arg": val, + } + err = cl.Call(context.Background(), "echo_ws", params) + require.Nil(t, err) + + msg := <-cl.ResponsesCh + if msg.Error != nil { + t.Fatal(err) + } + result := new(ResultEcho) + err = json.Unmarshal(msg.Result, result) + require.Nil(t, err) + got := result.Value + assert.Equal(t, got, val) +} + +func TestWSHandlesArrayParams(t *testing.T) { + cl := client.NewWSClient(tcpAddr, websocketEndpoint) + cl.SetLogger(log.TestingLogger()) + err := cl.Start() + require.Nil(t, err) + defer cl.Stop() + + val := "acbd" + params := []interface{}{val} + err = cl.CallWithArrayParams(context.Background(), "echo_ws", params) + require.Nil(t, err) + + msg := <-cl.ResponsesCh + if msg.Error != nil { + t.Fatalf("%+v", err) + } + result := new(ResultEcho) + err = json.Unmarshal(msg.Result, result) + require.Nil(t, err) + got := result.Value + assert.Equal(t, got, val) +} + +// TestWSClientPingPong checks that a client & server exchange pings +// & pongs so connection stays alive. +func TestWSClientPingPong(t *testing.T) { + cl := client.NewWSClient(tcpAddr, websocketEndpoint) + cl.SetLogger(log.TestingLogger()) + err := cl.Start() + require.Nil(t, err) + defer cl.Stop() + + time.Sleep(6 * time.Second) +} + +func randBytes(t *testing.T) []byte { + n := cmn.RandIntn(10) + 2 + buf := make([]byte, n) + _, err := crand.Read(buf) + require.Nil(t, err) + return bytes.Replace(buf, []byte("="), []byte{100}, -1) +} diff --git a/rpc/lib/server/handlers.go b/rpc/lib/server/handlers.go new file mode 100644 index 000000000..18b96f043 --- /dev/null +++ b/rpc/lib/server/handlers.go @@ -0,0 +1,838 @@ +package rpcserver + +import ( + "bytes" + "context" + "encoding/hex" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "reflect" + "runtime/debug" + "sort" + "strings" + "time" + + "github.com/gorilla/websocket" + "github.com/pkg/errors" + + amino "github.com/MinterTeam/go-amino" + types "github.com/MinterTeam/minter-go-node/rpc/lib/types" + cmn "github.com/tendermint/tendermint/libs/common" + "github.com/tendermint/tendermint/libs/log" +) + +// RegisterRPCFuncs adds a route for each function in the funcMap, as well as general jsonrpc and websocket handlers for all functions. +// "result" is the interface on which the result objects are registered, and is popualted with every RPCResponse +func RegisterRPCFuncs(mux *http.ServeMux, funcMap map[string]*RPCFunc, cdc *amino.Codec, logger log.Logger) { + // HTTP endpoints + for funcName, rpcFunc := range funcMap { + mux.HandleFunc("/"+funcName, makeHTTPHandler(rpcFunc, cdc, logger)) + } + + // JSONRPC endpoints + mux.HandleFunc("/", handleInvalidJSONRPCPaths(makeJSONRPCHandler(funcMap, cdc, logger))) +} + +//------------------------------------- +// function introspection + +// RPCFunc contains the introspected type information for a function +type RPCFunc struct { + f reflect.Value // underlying rpc function + args []reflect.Type // type of each function arg + returns []reflect.Type // type of each return arg + argNames []string // name of each argument + ws bool // websocket only +} + +// NewRPCFunc wraps a function for introspection. +// f is the function, args are comma separated argument names +func NewRPCFunc(f interface{}, args string) *RPCFunc { + return newRPCFunc(f, args, false) +} + +// NewWSRPCFunc wraps a function for introspection and use in the websockets. +func NewWSRPCFunc(f interface{}, args string) *RPCFunc { + return newRPCFunc(f, args, true) +} + +func newRPCFunc(f interface{}, args string, ws bool) *RPCFunc { + var argNames []string + if args != "" { + argNames = strings.Split(args, ",") + } + return &RPCFunc{ + f: reflect.ValueOf(f), + args: funcArgTypes(f), + returns: funcReturnTypes(f), + argNames: argNames, + ws: ws, + } +} + +// return a function's argument types +func funcArgTypes(f interface{}) []reflect.Type { + t := reflect.TypeOf(f) + n := t.NumIn() + typez := make([]reflect.Type, n) + for i := 0; i < n; i++ { + typez[i] = t.In(i) + } + return typez +} + +// return a function's return types +func funcReturnTypes(f interface{}) []reflect.Type { + t := reflect.TypeOf(f) + n := t.NumOut() + typez := make([]reflect.Type, n) + for i := 0; i < n; i++ { + typez[i] = t.Out(i) + } + return typez +} + +// function introspection +//----------------------------------------------------------------------------- +// rpc.json + +// jsonrpc calls grab the given method's function info and runs reflect.Call +func makeJSONRPCHandler(funcMap map[string]*RPCFunc, cdc *amino.Codec, logger log.Logger) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + b, err := ioutil.ReadAll(r.Body) + if err != nil { + WriteRPCResponseHTTP(w, types.RPCInvalidRequestError(types.JSONRPCStringID(""), errors.Wrap(err, "Error reading request body"))) + return + } + // if its an empty request (like from a browser), + // just display a list of functions + if len(b) == 0 { + writeListOfEndpoints(w, r, funcMap) + return + } + + var request types.RPCRequest + err = json.Unmarshal(b, &request) + if err != nil { + WriteRPCResponseHTTP(w, types.RPCParseError(types.JSONRPCStringID(""), errors.Wrap(err, "Error unmarshalling request"))) + return + } + // A Notification is a Request object without an "id" member. + // The Server MUST NOT reply to a Notification, including those that are within a batch request. + if request.ID == types.JSONRPCStringID("") { + logger.Debug("HTTPJSONRPC received a notification, skipping... (please send a non-empty ID if you want to call a method)") + return + } + if len(r.URL.Path) > 1 { + WriteRPCResponseHTTP(w, types.RPCInvalidRequestError(request.ID, errors.Errorf("Path %s is invalid", r.URL.Path))) + return + } + rpcFunc := funcMap[request.Method] + if rpcFunc == nil || rpcFunc.ws { + WriteRPCResponseHTTP(w, types.RPCMethodNotFoundError(request.ID)) + return + } + var args []reflect.Value + if len(request.Params) > 0 { + args, err = jsonParamsToArgsRPC(rpcFunc, cdc, request.Params) + if err != nil { + WriteRPCResponseHTTP(w, types.RPCInvalidParamsError(request.ID, errors.Wrap(err, "Error converting json params to arguments"))) + return + } + } + returns := rpcFunc.f.Call(args) + logger.Info("HTTPJSONRPC", "method", request.Method, "args", args, "returns", returns) + result, err := unreflectResult(returns) + if err != nil { + WriteRPCResponseHTTP(w, types.RPCInternalError(request.ID, err)) + return + } + WriteRPCResponseHTTP(w, types.NewRPCSuccessResponse(cdc, request.ID, result)) + } +} + +func handleInvalidJSONRPCPaths(next http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + // Since the pattern "/" matches all paths not matched by other registered patterns we check whether the path is indeed + // "/", otherwise return a 404 error + if r.URL.Path != "/" { + http.NotFound(w, r) + return + } + + next(w, r) + } +} + +func mapParamsToArgs(rpcFunc *RPCFunc, cdc *amino.Codec, params map[string]json.RawMessage, argsOffset int) ([]reflect.Value, error) { + values := make([]reflect.Value, len(rpcFunc.argNames)) + for i, argName := range rpcFunc.argNames { + argType := rpcFunc.args[i+argsOffset] + + if p, ok := params[argName]; ok && p != nil && len(p) > 0 { + val := reflect.New(argType) + err := cdc.UnmarshalJSON(p, val.Interface()) + if err != nil { + return nil, err + } + values[i] = val.Elem() + } else { // use default for that type + values[i] = reflect.Zero(argType) + } + } + + return values, nil +} + +func arrayParamsToArgs(rpcFunc *RPCFunc, cdc *amino.Codec, params []json.RawMessage, argsOffset int) ([]reflect.Value, error) { + if len(rpcFunc.argNames) != len(params) { + return nil, errors.Errorf("Expected %v parameters (%v), got %v (%v)", + len(rpcFunc.argNames), rpcFunc.argNames, len(params), params) + } + + values := make([]reflect.Value, len(params)) + for i, p := range params { + argType := rpcFunc.args[i+argsOffset] + val := reflect.New(argType) + err := cdc.UnmarshalJSON(p, val.Interface()) + if err != nil { + return nil, err + } + values[i] = val.Elem() + } + return values, nil +} + +// `raw` is unparsed json (from json.RawMessage) encoding either a map or an array. +// `argsOffset` should be 0 for RPC calls, and 1 for WS requests, where len(rpcFunc.args) != len(rpcFunc.argNames). +// +// Example: +// rpcFunc.args = [rpctypes.WSRPCContext string] +// rpcFunc.argNames = ["arg"] +func jsonParamsToArgs(rpcFunc *RPCFunc, cdc *amino.Codec, raw []byte, argsOffset int) ([]reflect.Value, error) { + + // TODO: Make more efficient, perhaps by checking the first character for '{' or '['? + // First, try to get the map. + var m map[string]json.RawMessage + err := json.Unmarshal(raw, &m) + if err == nil { + return mapParamsToArgs(rpcFunc, cdc, m, argsOffset) + } + + // Otherwise, try an array. + var a []json.RawMessage + err = json.Unmarshal(raw, &a) + if err == nil { + return arrayParamsToArgs(rpcFunc, cdc, a, argsOffset) + } + + // Otherwise, bad format, we cannot parse + return nil, errors.Errorf("Unknown type for JSON params: %v. Expected map or array", err) +} + +// Convert a []interface{} OR a map[string]interface{} to properly typed values +func jsonParamsToArgsRPC(rpcFunc *RPCFunc, cdc *amino.Codec, params json.RawMessage) ([]reflect.Value, error) { + return jsonParamsToArgs(rpcFunc, cdc, params, 0) +} + +// Same as above, but with the first param the websocket connection +func jsonParamsToArgsWS(rpcFunc *RPCFunc, cdc *amino.Codec, params json.RawMessage, wsCtx types.WSRPCContext) ([]reflect.Value, error) { + values, err := jsonParamsToArgs(rpcFunc, cdc, params, 1) + if err != nil { + return nil, err + } + return append([]reflect.Value{reflect.ValueOf(wsCtx)}, values...), nil +} + +// rpc.json +//----------------------------------------------------------------------------- +// rpc.http + +// convert from a function name to the http handler +func makeHTTPHandler(rpcFunc *RPCFunc, cdc *amino.Codec, logger log.Logger) func(http.ResponseWriter, *http.Request) { + // Exception for websocket endpoints + if rpcFunc.ws { + return func(w http.ResponseWriter, r *http.Request) { + WriteRPCResponseHTTP(w, types.RPCMethodNotFoundError(types.JSONRPCStringID(""))) + } + } + // All other endpoints + return func(w http.ResponseWriter, r *http.Request) { + logger.Debug("HTTP HANDLER", "req", r) + args, err := httpParamsToArgs(rpcFunc, cdc, r) + if err != nil { + WriteRPCResponseHTTP(w, types.RPCInvalidParamsError(types.JSONRPCStringID(""), errors.Wrap(err, "Error converting http params to arguments"))) + return + } + returns := rpcFunc.f.Call(args) + logger.Info("HTTPRestRPC", "method", r.URL.Path, "args", args, "returns", returns) + result, err := unreflectResult(returns) + if err != nil { + WriteRPCResponseHTTP(w, types.RPCInternalError(types.JSONRPCStringID(""), err)) + return + } + WriteRPCResponseHTTP(w, types.NewRPCSuccessResponse(cdc, types.JSONRPCStringID(""), result)) + } +} + +// Covert an http query to a list of properly typed values. +// To be properly decoded the arg must be a concrete type from tendermint (if its an interface). +func httpParamsToArgs(rpcFunc *RPCFunc, cdc *amino.Codec, r *http.Request) ([]reflect.Value, error) { + values := make([]reflect.Value, len(rpcFunc.args)) + + for i, name := range rpcFunc.argNames { + argType := rpcFunc.args[i] + + values[i] = reflect.Zero(argType) // set default for that type + + arg := GetParam(r, name) + // log.Notice("param to arg", "argType", argType, "name", name, "arg", arg) + + if "" == arg { + continue + } + + v, err, ok := nonJSONStringToArg(cdc, argType, arg) + if err != nil { + return nil, err + } + if ok { + values[i] = v + continue + } + + values[i], err = jsonStringToArg(cdc, argType, arg) + if err != nil { + return nil, err + } + } + + return values, nil +} + +func jsonStringToArg(cdc *amino.Codec, rt reflect.Type, arg string) (reflect.Value, error) { + rv := reflect.New(rt) + err := cdc.UnmarshalJSON([]byte(arg), rv.Interface()) + if err != nil { + return rv, err + } + rv = rv.Elem() + return rv, nil +} + +func nonJSONStringToArg(cdc *amino.Codec, rt reflect.Type, arg string) (reflect.Value, error, bool) { + if rt.Kind() == reflect.Ptr { + rv_, err, ok := nonJSONStringToArg(cdc, rt.Elem(), arg) + if err != nil { + return reflect.Value{}, err, false + } else if ok { + rv := reflect.New(rt.Elem()) + rv.Elem().Set(rv_) + return rv, nil, true + } else { + return reflect.Value{}, nil, false + } + } else { + return _nonJSONStringToArg(cdc, rt, arg) + } +} + +// NOTE: rt.Kind() isn't a pointer. +func _nonJSONStringToArg(cdc *amino.Codec, rt reflect.Type, arg string) (reflect.Value, error, bool) { + isIntString := RE_INT.Match([]byte(arg)) + isQuotedString := strings.HasPrefix(arg, `"`) && strings.HasSuffix(arg, `"`) + isHexString := strings.HasPrefix(strings.ToLower(arg), "0x") + + var expectingString, expectingByteSlice, expectingInt bool + switch rt.Kind() { + case reflect.Int, reflect.Uint, reflect.Int8, reflect.Uint8, reflect.Int16, reflect.Uint16, reflect.Int32, reflect.Uint32, reflect.Int64, reflect.Uint64: + expectingInt = true + case reflect.String: + expectingString = true + case reflect.Slice: + expectingByteSlice = rt.Elem().Kind() == reflect.Uint8 + } + + if isIntString && expectingInt { + qarg := `"` + arg + `"` + // jsonStringToArg + rv, err := jsonStringToArg(cdc, rt, qarg) + if err != nil { + return rv, err, false + } else { + return rv, nil, true + } + } + + if isHexString { + if !expectingString && !expectingByteSlice { + err := errors.Errorf("Got a hex string arg, but expected '%s'", + rt.Kind().String()) + return reflect.ValueOf(nil), err, false + } + + var value []byte + value, err := hex.DecodeString(arg[2:]) + if err != nil { + return reflect.ValueOf(nil), err, false + } + if rt.Kind() == reflect.String { + return reflect.ValueOf(string(value)), nil, true + } + return reflect.ValueOf([]byte(value)), nil, true + } + + if isQuotedString && expectingByteSlice { + v := reflect.New(reflect.TypeOf("")) + err := cdc.UnmarshalJSON([]byte(arg), v.Interface()) + if err != nil { + return reflect.ValueOf(nil), err, false + } + v = v.Elem() + return reflect.ValueOf([]byte(v.String())), nil, true + } + + return reflect.ValueOf(nil), nil, false +} + +// rpc.http +//----------------------------------------------------------------------------- +// rpc.websocket + +const ( + defaultWSWriteChanCapacity = 1000 + defaultWSWriteWait = 10 * time.Second + defaultWSReadWait = 30 * time.Second + defaultWSPingPeriod = (defaultWSReadWait * 9) / 10 +) + +// A single websocket connection contains listener id, underlying ws +// connection, and the event switch for subscribing to events. +// +// In case of an error, the connection is stopped. +type wsConnection struct { + cmn.BaseService + + remoteAddr string + baseConn *websocket.Conn + writeChan chan types.RPCResponse + + funcMap map[string]*RPCFunc + cdc *amino.Codec + + // write channel capacity + writeChanCapacity int + + // each write times out after this. + writeWait time.Duration + + // Connection times out if we haven't received *anything* in this long, not even pings. + readWait time.Duration + + // Send pings to server with this period. Must be less than readWait, but greater than zero. + pingPeriod time.Duration + + // object that is used to subscribe / unsubscribe from events + eventSub types.EventSubscriber +} + +// NewWSConnection wraps websocket.Conn. +// +// See the commentary on the func(*wsConnection) functions for a detailed +// description of how to configure ping period and pong wait time. NOTE: if the +// write buffer is full, pongs may be dropped, which may cause clients to +// disconnect. see https://github.com/gorilla/websocket/issues/97 +func NewWSConnection( + baseConn *websocket.Conn, + funcMap map[string]*RPCFunc, + cdc *amino.Codec, + options ...func(*wsConnection), +) *wsConnection { + baseConn.SetReadLimit(maxBodyBytes) + wsc := &wsConnection{ + remoteAddr: baseConn.RemoteAddr().String(), + baseConn: baseConn, + funcMap: funcMap, + cdc: cdc, + writeWait: defaultWSWriteWait, + writeChanCapacity: defaultWSWriteChanCapacity, + readWait: defaultWSReadWait, + pingPeriod: defaultWSPingPeriod, + } + for _, option := range options { + option(wsc) + } + wsc.BaseService = *cmn.NewBaseService(nil, "wsConnection", wsc) + return wsc +} + +// EventSubscriber sets object that is used to subscribe / unsubscribe from +// events - not Goroutine-safe. If none given, default node's eventBus will be +// used. +func EventSubscriber(eventSub types.EventSubscriber) func(*wsConnection) { + return func(wsc *wsConnection) { + wsc.eventSub = eventSub + } +} + +// WriteWait sets the amount of time to wait before a websocket write times out. +// It should only be used in the constructor - not Goroutine-safe. +func WriteWait(writeWait time.Duration) func(*wsConnection) { + return func(wsc *wsConnection) { + wsc.writeWait = writeWait + } +} + +// WriteChanCapacity sets the capacity of the websocket write channel. +// It should only be used in the constructor - not Goroutine-safe. +func WriteChanCapacity(cap int) func(*wsConnection) { + return func(wsc *wsConnection) { + wsc.writeChanCapacity = cap + } +} + +// ReadWait sets the amount of time to wait before a websocket read times out. +// It should only be used in the constructor - not Goroutine-safe. +func ReadWait(readWait time.Duration) func(*wsConnection) { + return func(wsc *wsConnection) { + wsc.readWait = readWait + } +} + +// PingPeriod sets the duration for sending websocket pings. +// It should only be used in the constructor - not Goroutine-safe. +func PingPeriod(pingPeriod time.Duration) func(*wsConnection) { + return func(wsc *wsConnection) { + wsc.pingPeriod = pingPeriod + } +} + +// OnStart implements cmn.Service by starting the read and write routines. It +// blocks until the connection closes. +func (wsc *wsConnection) OnStart() error { + wsc.writeChan = make(chan types.RPCResponse, wsc.writeChanCapacity) + + // Read subscriptions/unsubscriptions to events + go wsc.readRoutine() + // Write responses, BLOCKING. + wsc.writeRoutine() + + return nil +} + +// OnStop implements cmn.Service by unsubscribing remoteAddr from all subscriptions. +func (wsc *wsConnection) OnStop() { + // Both read and write loops close the websocket connection when they exit their loops. + // The writeChan is never closed, to allow WriteRPCResponse() to fail. + if wsc.eventSub != nil { + wsc.eventSub.UnsubscribeAll(context.TODO(), wsc.remoteAddr) + } +} + +// GetRemoteAddr returns the remote address of the underlying connection. +// It implements WSRPCConnection +func (wsc *wsConnection) GetRemoteAddr() string { + return wsc.remoteAddr +} + +// GetEventSubscriber implements WSRPCConnection by returning event subscriber. +func (wsc *wsConnection) GetEventSubscriber() types.EventSubscriber { + return wsc.eventSub +} + +// WriteRPCResponse pushes a response to the writeChan, and blocks until it is accepted. +// It implements WSRPCConnection. It is Goroutine-safe. +func (wsc *wsConnection) WriteRPCResponse(resp types.RPCResponse) { + select { + case <-wsc.Quit(): + return + case wsc.writeChan <- resp: + } +} + +// TryWriteRPCResponse attempts to push a response to the writeChan, but does not block. +// It implements WSRPCConnection. It is Goroutine-safe +func (wsc *wsConnection) TryWriteRPCResponse(resp types.RPCResponse) bool { + select { + case <-wsc.Quit(): + return false + case wsc.writeChan <- resp: + return true + default: + return false + } +} + +// Codec returns an amino codec used to decode parameters and encode results. +// It implements WSRPCConnection. +func (wsc *wsConnection) Codec() *amino.Codec { + return wsc.cdc +} + +// Read from the socket and subscribe to or unsubscribe from events +func (wsc *wsConnection) readRoutine() { + defer func() { + if r := recover(); r != nil { + err, ok := r.(error) + if !ok { + err = fmt.Errorf("WSJSONRPC: %v", r) + } + wsc.Logger.Error("Panic in WSJSONRPC handler", "err", err, "stack", string(debug.Stack())) + wsc.WriteRPCResponse(types.RPCInternalError(types.JSONRPCStringID("unknown"), err)) + go wsc.readRoutine() + } else { + wsc.baseConn.Close() // nolint: errcheck + } + }() + + wsc.baseConn.SetPongHandler(func(m string) error { + return wsc.baseConn.SetReadDeadline(time.Now().Add(wsc.readWait)) + }) + + for { + select { + case <-wsc.Quit(): + return + default: + // reset deadline for every type of message (control or data) + if err := wsc.baseConn.SetReadDeadline(time.Now().Add(wsc.readWait)); err != nil { + wsc.Logger.Error("failed to set read deadline", "err", err) + } + var in []byte + _, in, err := wsc.baseConn.ReadMessage() + if err != nil { + if websocket.IsCloseError(err, websocket.CloseNormalClosure) { + wsc.Logger.Info("Client closed the connection") + } else { + wsc.Logger.Error("Failed to read request", "err", err) + } + wsc.Stop() + return + } + + var request types.RPCRequest + err = json.Unmarshal(in, &request) + if err != nil { + wsc.WriteRPCResponse(types.RPCParseError(types.JSONRPCStringID(""), errors.Wrap(err, "Error unmarshaling request"))) + continue + } + + // A Notification is a Request object without an "id" member. + // The Server MUST NOT reply to a Notification, including those that are within a batch request. + if request.ID == types.JSONRPCStringID("") { + wsc.Logger.Debug("WSJSONRPC received a notification, skipping... (please send a non-empty ID if you want to call a method)") + continue + } + + // Now, fetch the RPCFunc and execute it. + + rpcFunc := wsc.funcMap[request.Method] + if rpcFunc == nil { + wsc.WriteRPCResponse(types.RPCMethodNotFoundError(request.ID)) + continue + } + var args []reflect.Value + if rpcFunc.ws { + wsCtx := types.WSRPCContext{Request: request, WSRPCConnection: wsc} + if len(request.Params) > 0 { + args, err = jsonParamsToArgsWS(rpcFunc, wsc.cdc, request.Params, wsCtx) + } + } else { + if len(request.Params) > 0 { + args, err = jsonParamsToArgsRPC(rpcFunc, wsc.cdc, request.Params) + } + } + if err != nil { + wsc.WriteRPCResponse(types.RPCInternalError(request.ID, errors.Wrap(err, "Error converting json params to arguments"))) + continue + } + returns := rpcFunc.f.Call(args) + + // TODO: Need to encode args/returns to string if we want to log them + wsc.Logger.Info("WSJSONRPC", "method", request.Method) + + result, err := unreflectResult(returns) + if err != nil { + wsc.WriteRPCResponse(types.RPCInternalError(request.ID, err)) + continue + } + + wsc.WriteRPCResponse(types.NewRPCSuccessResponse(wsc.cdc, request.ID, result)) + } + } +} + +// receives on a write channel and writes out on the socket +func (wsc *wsConnection) writeRoutine() { + pingTicker := time.NewTicker(wsc.pingPeriod) + defer func() { + pingTicker.Stop() + if err := wsc.baseConn.Close(); err != nil { + wsc.Logger.Error("Error closing connection", "err", err) + } + }() + + // https://github.com/gorilla/websocket/issues/97 + pongs := make(chan string, 1) + wsc.baseConn.SetPingHandler(func(m string) error { + select { + case pongs <- m: + default: + } + return nil + }) + + for { + select { + case m := <-pongs: + err := wsc.writeMessageWithDeadline(websocket.PongMessage, []byte(m)) + if err != nil { + wsc.Logger.Info("Failed to write pong (client may disconnect)", "err", err) + } + case <-pingTicker.C: + err := wsc.writeMessageWithDeadline(websocket.PingMessage, []byte{}) + if err != nil { + wsc.Logger.Error("Failed to write ping", "err", err) + wsc.Stop() + return + } + case msg := <-wsc.writeChan: + jsonBytes, err := json.MarshalIndent(msg, "", " ") + if err != nil { + wsc.Logger.Error("Failed to marshal RPCResponse to JSON", "err", err) + } else { + if err = wsc.writeMessageWithDeadline(websocket.TextMessage, jsonBytes); err != nil { + wsc.Logger.Error("Failed to write response", "err", err) + wsc.Stop() + return + } + } + case <-wsc.Quit(): + return + } + } +} + +// All writes to the websocket must (re)set the write deadline. +// If some writes don't set it while others do, they may timeout incorrectly (https://github.com/tendermint/tendermint/issues/553) +func (wsc *wsConnection) writeMessageWithDeadline(msgType int, msg []byte) error { + if err := wsc.baseConn.SetWriteDeadline(time.Now().Add(wsc.writeWait)); err != nil { + return err + } + return wsc.baseConn.WriteMessage(msgType, msg) +} + +//---------------------------------------- + +// WebsocketManager provides a WS handler for incoming connections and passes a +// map of functions along with any additional params to new connections. +// NOTE: The websocket path is defined externally, e.g. in node/node.go +type WebsocketManager struct { + websocket.Upgrader + + funcMap map[string]*RPCFunc + cdc *amino.Codec + logger log.Logger + wsConnOptions []func(*wsConnection) +} + +// NewWebsocketManager returns a new WebsocketManager that passes a map of +// functions, connection options and logger to new WS connections. +func NewWebsocketManager(funcMap map[string]*RPCFunc, cdc *amino.Codec, wsConnOptions ...func(*wsConnection)) *WebsocketManager { + return &WebsocketManager{ + funcMap: funcMap, + cdc: cdc, + Upgrader: websocket.Upgrader{ + CheckOrigin: func(r *http.Request) bool { + // TODO ??? + return true + }, + }, + logger: log.NewNopLogger(), + wsConnOptions: wsConnOptions, + } +} + +// SetLogger sets the logger. +func (wm *WebsocketManager) SetLogger(l log.Logger) { + wm.logger = l +} + +// WebsocketHandler upgrades the request/response (via http.Hijack) and starts +// the wsConnection. +func (wm *WebsocketManager) WebsocketHandler(w http.ResponseWriter, r *http.Request) { + wsConn, err := wm.Upgrade(w, r, nil) + if err != nil { + // TODO - return http error + wm.logger.Error("Failed to upgrade to websocket connection", "err", err) + return + } + + // register connection + con := NewWSConnection(wsConn, wm.funcMap, wm.cdc, wm.wsConnOptions...) + con.SetLogger(wm.logger.With("remote", wsConn.RemoteAddr())) + wm.logger.Info("New websocket connection", "remote", con.remoteAddr) + err = con.Start() // Blocking + if err != nil { + wm.logger.Error("Error starting connection", "err", err) + } +} + +// rpc.websocket +//----------------------------------------------------------------------------- + +// NOTE: assume returns is result struct and error. If error is not nil, return it +func unreflectResult(returns []reflect.Value) (interface{}, error) { + errV := returns[1] + if errV.Interface() != nil { + return nil, errors.Errorf("%v", errV.Interface()) + } + rv := returns[0] + // the result is a registered interface, + // we need a pointer to it so we can marshal with type byte + rvp := reflect.New(rv.Type()) + rvp.Elem().Set(rv) + return rvp.Interface(), nil +} + +// writes a list of available rpc endpoints as an html page +func writeListOfEndpoints(w http.ResponseWriter, r *http.Request, funcMap map[string]*RPCFunc) { + noArgNames := []string{} + argNames := []string{} + for name, funcData := range funcMap { + if len(funcData.args) == 0 { + noArgNames = append(noArgNames, name) + } else { + argNames = append(argNames, name) + } + } + sort.Strings(noArgNames) + sort.Strings(argNames) + buf := new(bytes.Buffer) + buf.WriteString("") + buf.WriteString("
Available endpoints:
") + + for _, name := range noArgNames { + link := fmt.Sprintf("//%s/%s", r.Host, name) + buf.WriteString(fmt.Sprintf("%s
", link, link)) + } + + buf.WriteString("
Endpoints that require arguments:
") + for _, name := range argNames { + link := fmt.Sprintf("//%s/%s?", r.Host, name) + funcData := funcMap[name] + for i, argName := range funcData.argNames { + link += argName + "=_" + if i < len(funcData.argNames)-1 { + link += "&" + } + } + buf.WriteString(fmt.Sprintf("%s
", link, link)) + } + buf.WriteString("") + w.Header().Set("Content-Type", "text/html") + w.WriteHeader(200) + w.Write(buf.Bytes()) // nolint: errcheck +} diff --git a/rpc/lib/server/handlers_test.go b/rpc/lib/server/handlers_test.go new file mode 100644 index 000000000..46db40aa2 --- /dev/null +++ b/rpc/lib/server/handlers_test.go @@ -0,0 +1,207 @@ +package rpcserver_test + +import ( + "bytes" + "encoding/json" + "io/ioutil" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/gorilla/websocket" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + amino "github.com/MinterTeam/go-amino" + "github.com/tendermint/tendermint/libs/log" + rs "github.com/tendermint/tendermint/rpc/lib/server" + types "github.com/tendermint/tendermint/rpc/lib/types" +) + +////////////////////////////////////////////////////////////////////////////// +// HTTP REST API +// TODO + +////////////////////////////////////////////////////////////////////////////// +// JSON-RPC over HTTP + +func testMux() *http.ServeMux { + funcMap := map[string]*rs.RPCFunc{ + "c": rs.NewRPCFunc(func(s string, i int) (string, error) { return "foo", nil }, "s,i"), + } + cdc := amino.NewCodec() + mux := http.NewServeMux() + buf := new(bytes.Buffer) + logger := log.NewTMLogger(buf) + rs.RegisterRPCFuncs(mux, funcMap, cdc, logger) + + return mux +} + +func statusOK(code int) bool { return code >= 200 && code <= 299 } + +// Ensure that nefarious/unintended inputs to `params` +// do not crash our RPC handlers. +// See Issue https://github.com/tendermint/tendermint/issues/708. +func TestRPCParams(t *testing.T) { + mux := testMux() + tests := []struct { + payload string + wantErr string + expectedId interface{} + }{ + // bad + {`{"jsonrpc": "2.0", "id": "0"}`, "Method not found", types.JSONRPCStringID("0")}, + {`{"jsonrpc": "2.0", "method": "y", "id": "0"}`, "Method not found", types.JSONRPCStringID("0")}, + {`{"method": "c", "id": "0", "params": a}`, "invalid character", types.JSONRPCStringID("")}, // id not captured in JSON parsing failures + {`{"method": "c", "id": "0", "params": ["a"]}`, "got 1", types.JSONRPCStringID("0")}, + {`{"method": "c", "id": "0", "params": ["a", "b"]}`, "invalid character", types.JSONRPCStringID("0")}, + {`{"method": "c", "id": "0", "params": [1, 1]}`, "of type string", types.JSONRPCStringID("0")}, + + // good + {`{"jsonrpc": "2.0", "method": "c", "id": "0", "params": null}`, "", types.JSONRPCStringID("0")}, + {`{"method": "c", "id": "0", "params": {}}`, "", types.JSONRPCStringID("0")}, + {`{"method": "c", "id": "0", "params": ["a", "10"]}`, "", types.JSONRPCStringID("0")}, + } + + for i, tt := range tests { + req, _ := http.NewRequest("POST", "http://localhost/", strings.NewReader(tt.payload)) + rec := httptest.NewRecorder() + mux.ServeHTTP(rec, req) + res := rec.Result() + // Always expecting back a JSONRPCResponse + assert.True(t, statusOK(res.StatusCode), "#%d: should always return 2XX", i) + blob, err := ioutil.ReadAll(res.Body) + if err != nil { + t.Errorf("#%d: err reading body: %v", i, err) + continue + } + + recv := new(types.RPCResponse) + assert.Nil(t, json.Unmarshal(blob, recv), "#%d: expecting successful parsing of an RPCResponse:\nblob: %s", i, blob) + assert.NotEqual(t, recv, new(types.RPCResponse), "#%d: not expecting a blank RPCResponse", i) + assert.Equal(t, tt.expectedId, recv.ID, "#%d: expected ID not matched in RPCResponse", i) + if tt.wantErr == "" { + assert.Nil(t, recv.Error, "#%d: not expecting an error", i) + } else { + assert.True(t, recv.Error.Code < 0, "#%d: not expecting a positive JSONRPC code", i) + // The wanted error is either in the message or the data + assert.Contains(t, recv.Error.Message+recv.Error.Data, tt.wantErr, "#%d: expected substring", i) + } + } +} + +func TestJSONRPCID(t *testing.T) { + mux := testMux() + tests := []struct { + payload string + wantErr bool + expectedId interface{} + }{ + // good id + {`{"jsonrpc": "2.0", "method": "c", "id": "0", "params": ["a", "10"]}`, false, types.JSONRPCStringID("0")}, + {`{"jsonrpc": "2.0", "method": "c", "id": "abc", "params": ["a", "10"]}`, false, types.JSONRPCStringID("abc")}, + {`{"jsonrpc": "2.0", "method": "c", "id": 0, "params": ["a", "10"]}`, false, types.JSONRPCIntID(0)}, + {`{"jsonrpc": "2.0", "method": "c", "id": 1, "params": ["a", "10"]}`, false, types.JSONRPCIntID(1)}, + {`{"jsonrpc": "2.0", "method": "c", "id": 1.3, "params": ["a", "10"]}`, false, types.JSONRPCIntID(1)}, + {`{"jsonrpc": "2.0", "method": "c", "id": -1, "params": ["a", "10"]}`, false, types.JSONRPCIntID(-1)}, + {`{"jsonrpc": "2.0", "method": "c", "id": null, "params": ["a", "10"]}`, false, nil}, + + // bad id + {`{"jsonrpc": "2.0", "method": "c", "id": {}, "params": ["a", "10"]}`, true, nil}, + {`{"jsonrpc": "2.0", "method": "c", "id": [], "params": ["a", "10"]}`, true, nil}, + } + + for i, tt := range tests { + req, _ := http.NewRequest("POST", "http://localhost/", strings.NewReader(tt.payload)) + rec := httptest.NewRecorder() + mux.ServeHTTP(rec, req) + res := rec.Result() + // Always expecting back a JSONRPCResponse + assert.True(t, statusOK(res.StatusCode), "#%d: should always return 2XX", i) + blob, err := ioutil.ReadAll(res.Body) + if err != nil { + t.Errorf("#%d: err reading body: %v", i, err) + continue + } + + recv := new(types.RPCResponse) + err = json.Unmarshal(blob, recv) + assert.Nil(t, err, "#%d: expecting successful parsing of an RPCResponse:\nblob: %s", i, blob) + if !tt.wantErr { + assert.NotEqual(t, recv, new(types.RPCResponse), "#%d: not expecting a blank RPCResponse", i) + assert.Equal(t, tt.expectedId, recv.ID, "#%d: expected ID not matched in RPCResponse", i) + assert.Nil(t, recv.Error, "#%d: not expecting an error", i) + } else { + assert.True(t, recv.Error.Code < 0, "#%d: not expecting a positive JSONRPC code", i) + } + } +} + +func TestRPCNotification(t *testing.T) { + mux := testMux() + body := strings.NewReader(`{"jsonrpc": "2.0", "id": ""}`) + req, _ := http.NewRequest("POST", "http://localhost/", body) + rec := httptest.NewRecorder() + mux.ServeHTTP(rec, req) + res := rec.Result() + + // Always expecting back a JSONRPCResponse + require.True(t, statusOK(res.StatusCode), "should always return 2XX") + blob, err := ioutil.ReadAll(res.Body) + require.Nil(t, err, "reading from the body should not give back an error") + require.Equal(t, len(blob), 0, "a notification SHOULD NOT be responded to by the server") +} + +func TestUnknownRPCPath(t *testing.T) { + mux := testMux() + req, _ := http.NewRequest("GET", "http://localhost/unknownrpcpath", nil) + rec := httptest.NewRecorder() + mux.ServeHTTP(rec, req) + res := rec.Result() + + // Always expecting back a 404 error + require.Equal(t, http.StatusNotFound, res.StatusCode, "should always return 404") +} + +////////////////////////////////////////////////////////////////////////////// +// JSON-RPC over WEBSOCKETS + +func TestWebsocketManagerHandler(t *testing.T) { + s := newWSServer() + defer s.Close() + + // check upgrader works + d := websocket.Dialer{} + c, dialResp, err := d.Dial("ws://"+s.Listener.Addr().String()+"/websocket", nil) + require.NoError(t, err) + + if got, want := dialResp.StatusCode, http.StatusSwitchingProtocols; got != want { + t.Errorf("dialResp.StatusCode = %q, want %q", got, want) + } + + // check basic functionality works + req, err := types.MapToRequest(amino.NewCodec(), types.JSONRPCStringID("TestWebsocketManager"), "c", map[string]interface{}{"s": "a", "i": 10}) + require.NoError(t, err) + err = c.WriteJSON(req) + require.NoError(t, err) + + var resp types.RPCResponse + err = c.ReadJSON(&resp) + require.NoError(t, err) + require.Nil(t, resp.Error) +} + +func newWSServer() *httptest.Server { + funcMap := map[string]*rs.RPCFunc{ + "c": rs.NewWSRPCFunc(func(wsCtx types.WSRPCContext, s string, i int) (string, error) { return "foo", nil }, "s,i"), + } + wm := rs.NewWebsocketManager(funcMap, amino.NewCodec()) + wm.SetLogger(log.TestingLogger()) + + mux := http.NewServeMux() + mux.HandleFunc("/websocket", wm.WebsocketHandler) + + return httptest.NewServer(mux) +} diff --git a/rpc/lib/server/http_params.go b/rpc/lib/server/http_params.go new file mode 100644 index 000000000..3c948c0ba --- /dev/null +++ b/rpc/lib/server/http_params.go @@ -0,0 +1,91 @@ +package rpcserver + +import ( + "encoding/hex" + "net/http" + "regexp" + "strconv" + + "github.com/pkg/errors" +) + +var ( + // Parts of regular expressions + atom = "[A-Z0-9!#$%&'*+\\-/=?^_`{|}~]+" + dotAtom = atom + `(?:\.` + atom + `)*` + domain = `[A-Z0-9.-]+\.[A-Z]{2,4}` + + RE_INT = regexp.MustCompile(`^-?[0-9]+$`) + RE_HEX = regexp.MustCompile(`^(?i)[a-f0-9]+$`) + RE_EMAIL = regexp.MustCompile(`^(?i)(` + dotAtom + `)@(` + dotAtom + `)$`) + RE_ADDRESS = regexp.MustCompile(`^(?i)[a-z0-9]{25,34}$`) + RE_HOST = regexp.MustCompile(`^(?i)(` + domain + `)$`) + + //RE_ID12 = regexp.MustCompile(`^[a-zA-Z0-9]{12}$`) +) + +func GetParam(r *http.Request, param string) string { + s := r.URL.Query().Get(param) + if s == "" { + s = r.FormValue(param) + } + return s +} + +func GetParamByteSlice(r *http.Request, param string) ([]byte, error) { + s := GetParam(r, param) + return hex.DecodeString(s) +} + +func GetParamInt64(r *http.Request, param string) (int64, error) { + s := GetParam(r, param) + i, err := strconv.ParseInt(s, 10, 64) + if err != nil { + return 0, errors.Errorf(param, err.Error()) + } + return i, nil +} + +func GetParamInt32(r *http.Request, param string) (int32, error) { + s := GetParam(r, param) + i, err := strconv.ParseInt(s, 10, 32) + if err != nil { + return 0, errors.Errorf(param, err.Error()) + } + return int32(i), nil +} + +func GetParamUint64(r *http.Request, param string) (uint64, error) { + s := GetParam(r, param) + i, err := strconv.ParseUint(s, 10, 64) + if err != nil { + return 0, errors.Errorf(param, err.Error()) + } + return i, nil +} + +func GetParamUint(r *http.Request, param string) (uint, error) { + s := GetParam(r, param) + i, err := strconv.ParseUint(s, 10, 64) + if err != nil { + return 0, errors.Errorf(param, err.Error()) + } + return uint(i), nil +} + +func GetParamRegexp(r *http.Request, param string, re *regexp.Regexp) (string, error) { + s := GetParam(r, param) + if !re.MatchString(s) { + return "", errors.Errorf(param, "Did not match regular expression %v", re.String()) + } + return s, nil +} + +func GetParamFloat64(r *http.Request, param string) (float64, error) { + s := GetParam(r, param) + f, err := strconv.ParseFloat(s, 64) + if err != nil { + return 0, errors.Errorf(param, err.Error()) + } + return f, nil +} diff --git a/rpc/lib/server/http_server.go b/rpc/lib/server/http_server.go new file mode 100644 index 000000000..5c10a68f7 --- /dev/null +++ b/rpc/lib/server/http_server.go @@ -0,0 +1,201 @@ +// Commons for HTTP handling +package rpcserver + +import ( + "bufio" + "encoding/json" + "fmt" + "net" + "net/http" + "runtime/debug" + "strings" + "time" + + "github.com/pkg/errors" + "golang.org/x/net/netutil" + + types "github.com/MinterTeam/minter-go-node/rpc/lib/types" + "github.com/tendermint/tendermint/libs/log" +) + +// Config is an RPC server configuration. +type Config struct { + MaxOpenConnections int +} + +const ( + // maxBodyBytes controls the maximum number of bytes the + // server will read parsing the request body. + maxBodyBytes = int64(1000000) // 1MB + + // same as the net/http default + maxHeaderBytes = 1 << 20 + + // Timeouts for reading/writing to the http connection. + // Public so handlers can read them - + // /broadcast_tx_commit has it's own timeout, which should + // be less than the WriteTimeout here. + // TODO: use a config instead. + ReadTimeout = 3 * time.Second + WriteTimeout = 20 * time.Second +) + +// StartHTTPServer takes a listener and starts an HTTP server with the given handler. +// It wraps handler with RecoverAndLogHandler. +// NOTE: This function blocks - you may want to call it in a go-routine. +func StartHTTPServer(listener net.Listener, handler http.Handler, logger log.Logger) error { + logger.Info(fmt.Sprintf("Starting RPC HTTP server on %s", listener.Addr())) + s := &http.Server{ + Handler: RecoverAndLogHandler(maxBytesHandler{h: handler, n: maxBodyBytes}, logger), + ReadTimeout: ReadTimeout, + WriteTimeout: WriteTimeout, + MaxHeaderBytes: maxHeaderBytes, + } + err := s.Serve(listener) + logger.Info("RPC HTTP server stopped", "err", err) + return err +} + +// StartHTTPAndTLSServer takes a listener and starts an HTTPS server with the given handler. +// It wraps handler with RecoverAndLogHandler. +// NOTE: This function blocks - you may want to call it in a go-routine. +func StartHTTPAndTLSServer( + listener net.Listener, + handler http.Handler, + certFile, keyFile string, + logger log.Logger, +) error { + logger.Info(fmt.Sprintf("Starting RPC HTTPS server on %s (cert: %q, key: %q)", + listener.Addr(), certFile, keyFile)) + s := &http.Server{ + Handler: RecoverAndLogHandler(maxBytesHandler{h: handler, n: maxBodyBytes}, logger), + ReadTimeout: ReadTimeout, + WriteTimeout: WriteTimeout, + MaxHeaderBytes: maxHeaderBytes, + } + err := s.ServeTLS(listener, certFile, keyFile) + + logger.Error("RPC HTTPS server stopped", "err", err) + return err +} + +func WriteRPCResponseHTTPError( + w http.ResponseWriter, + httpCode int, + res types.RPCResponse, +) { + jsonBytes, err := json.MarshalIndent(res, "", " ") + if err != nil { + panic(err) + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(httpCode) + w.Write(jsonBytes) // nolint: errcheck, gas +} + +func WriteRPCResponseHTTP(w http.ResponseWriter, res types.RPCResponse) { + jsonBytes, err := json.MarshalIndent(res, "", " ") + if err != nil { + panic(err) + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + w.Write(jsonBytes) // nolint: errcheck, gas +} + +//----------------------------------------------------------------------------- + +// Wraps an HTTP handler, adding error logging. +// If the inner function panics, the outer function recovers, logs, sends an +// HTTP 500 error response. +func RecoverAndLogHandler(handler http.Handler, logger log.Logger) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Wrap the ResponseWriter to remember the status + rww := &ResponseWriterWrapper{-1, w} + begin := time.Now() + + rww.Header().Set("X-Server-Time", fmt.Sprintf("%v", begin.Unix())) + + defer func() { + // Send a 500 error if a panic happens during a handler. + // Without this, Chrome & Firefox were retrying aborted ajax requests, + // at least to my localhost. + if e := recover(); e != nil { + + // If RPCResponse + if res, ok := e.(types.RPCResponse); ok { + WriteRPCResponseHTTP(rww, res) + } else { + // For the rest, + logger.Error( + "Panic in RPC HTTP handler", "err", e, "stack", + string(debug.Stack()), + ) + WriteRPCResponseHTTPError(rww, http.StatusInternalServerError, types.RPCInternalError(types.JSONRPCStringID(""), e.(error))) + } + } + + // Finally, log. + durationMS := time.Since(begin).Nanoseconds() / 1000000 + if rww.Status == -1 { + rww.Status = 200 + } + logger.Info("Served RPC HTTP response", + "method", r.Method, "url", r.URL, + "status", rww.Status, "duration", durationMS, + "remoteAddr", r.RemoteAddr, + ) + }() + + handler.ServeHTTP(rww, r) + }) +} + +// Remember the status for logging +type ResponseWriterWrapper struct { + Status int + http.ResponseWriter +} + +func (w *ResponseWriterWrapper) WriteHeader(status int) { + w.Status = status + w.ResponseWriter.WriteHeader(status) +} + +// implements http.Hijacker +func (w *ResponseWriterWrapper) Hijack() (net.Conn, *bufio.ReadWriter, error) { + return w.ResponseWriter.(http.Hijacker).Hijack() +} + +type maxBytesHandler struct { + h http.Handler + n int64 +} + +func (h maxBytesHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + r.Body = http.MaxBytesReader(w, r.Body, h.n) + h.h.ServeHTTP(w, r) +} + +// Listen starts a new net.Listener on the given address. +// It returns an error if the address is invalid or the call to Listen() fails. +func Listen(addr string, config Config) (listener net.Listener, err error) { + parts := strings.SplitN(addr, "://", 2) + if len(parts) != 2 { + return nil, errors.Errorf( + "Invalid listening address %s (use fully formed addresses, including the tcp:// or unix:// prefix)", + addr, + ) + } + proto, addr := parts[0], parts[1] + listener, err = net.Listen(proto, addr) + if err != nil { + return nil, errors.Errorf("Failed to listen on %v: %v", addr, err) + } + if config.MaxOpenConnections > 0 { + listener = netutil.LimitListener(listener, config.MaxOpenConnections) + } + + return listener, nil +} diff --git a/rpc/lib/server/http_server_test.go b/rpc/lib/server/http_server_test.go new file mode 100644 index 000000000..6b852afae --- /dev/null +++ b/rpc/lib/server/http_server_test.go @@ -0,0 +1,79 @@ +package rpcserver + +import ( + "fmt" + "io" + "io/ioutil" + "net/http" + "os" + "sync" + "sync/atomic" + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/tendermint/tendermint/libs/log" +) + +func TestMaxOpenConnections(t *testing.T) { + const max = 5 // max simultaneous connections + + // Start the server. + var open int32 + mux := http.NewServeMux() + mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + if n := atomic.AddInt32(&open, 1); n > int32(max) { + t.Errorf("%d open connections, want <= %d", n, max) + } + defer atomic.AddInt32(&open, -1) + time.Sleep(10 * time.Millisecond) + fmt.Fprint(w, "some body") + }) + l, err := Listen("tcp://127.0.0.1:0", Config{MaxOpenConnections: max}) + require.NoError(t, err) + defer l.Close() + go StartHTTPServer(l, mux, log.TestingLogger()) + + // Make N GET calls to the server. + attempts := max * 2 + var wg sync.WaitGroup + var failed int32 + for i := 0; i < attempts; i++ { + wg.Add(1) + go func() { + defer wg.Done() + c := http.Client{Timeout: 3 * time.Second} + r, err := c.Get("http://" + l.Addr().String()) + if err != nil { + t.Log(err) + atomic.AddInt32(&failed, 1) + return + } + defer r.Body.Close() + io.Copy(ioutil.Discard, r.Body) + }() + } + wg.Wait() + + // We expect some Gets to fail as the server's accept queue is filled, + // but most should succeed. + if int(failed) >= attempts/2 { + t.Errorf("%d requests failed within %d attempts", failed, attempts) + } +} + +func TestStartHTTPAndTLSServer(t *testing.T) { + // set up fixtures + listenerAddr := "tcp://0.0.0.0:0" + listener, err := Listen(listenerAddr, Config{MaxOpenConnections: 1}) + require.NoError(t, err) + mux := http.NewServeMux() + mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {}) + + // test failure + err = StartHTTPAndTLSServer(listener, mux, "", "", log.TestingLogger()) + require.IsType(t, (*os.PathError)(nil), err) + + // TODO: test that starting the server can actually work +} diff --git a/rpc/lib/server/parse_test.go b/rpc/lib/server/parse_test.go new file mode 100644 index 000000000..69e8a628a --- /dev/null +++ b/rpc/lib/server/parse_test.go @@ -0,0 +1,221 @@ +package rpcserver + +import ( + "encoding/json" + "fmt" + "net/http" + "strconv" + "testing" + + amino "github.com/MinterTeam/go-amino" + "github.com/stretchr/testify/assert" + cmn "github.com/tendermint/tendermint/libs/common" +) + +func TestParseJSONMap(t *testing.T) { + assert := assert.New(t) + + input := []byte(`{"value":"1234","height":22}`) + + // naive is float,string + var p1 map[string]interface{} + err := json.Unmarshal(input, &p1) + if assert.Nil(err) { + h, ok := p1["height"].(float64) + if assert.True(ok, "%#v", p1["height"]) { + assert.EqualValues(22, h) + } + v, ok := p1["value"].(string) + if assert.True(ok, "%#v", p1["value"]) { + assert.EqualValues("1234", v) + } + } + + // preloading map with values doesn't help + tmp := 0 + p2 := map[string]interface{}{ + "value": &cmn.HexBytes{}, + "height": &tmp, + } + err = json.Unmarshal(input, &p2) + if assert.Nil(err) { + h, ok := p2["height"].(float64) + if assert.True(ok, "%#v", p2["height"]) { + assert.EqualValues(22, h) + } + v, ok := p2["value"].(string) + if assert.True(ok, "%#v", p2["value"]) { + assert.EqualValues("1234", v) + } + } + + // preload here with *pointers* to the desired types + // struct has unknown types, but hard-coded keys + tmp = 0 + p3 := struct { + Value interface{} `json:"value"` + Height interface{} `json:"height"` + }{ + Height: &tmp, + Value: &cmn.HexBytes{}, + } + err = json.Unmarshal(input, &p3) + if assert.Nil(err) { + h, ok := p3.Height.(*int) + if assert.True(ok, "%#v", p3.Height) { + assert.Equal(22, *h) + } + v, ok := p3.Value.(*cmn.HexBytes) + if assert.True(ok, "%#v", p3.Value) { + assert.EqualValues([]byte{0x12, 0x34}, *v) + } + } + + // simplest solution, but hard-coded + p4 := struct { + Value cmn.HexBytes `json:"value"` + Height int `json:"height"` + }{} + err = json.Unmarshal(input, &p4) + if assert.Nil(err) { + assert.EqualValues(22, p4.Height) + assert.EqualValues([]byte{0x12, 0x34}, p4.Value) + } + + // so, let's use this trick... + // dynamic keys on map, and we can deserialize to the desired types + var p5 map[string]*json.RawMessage + err = json.Unmarshal(input, &p5) + if assert.Nil(err) { + var h int + err = json.Unmarshal(*p5["height"], &h) + if assert.Nil(err) { + assert.Equal(22, h) + } + + var v cmn.HexBytes + err = json.Unmarshal(*p5["value"], &v) + if assert.Nil(err) { + assert.Equal(cmn.HexBytes{0x12, 0x34}, v) + } + } +} + +func TestParseJSONArray(t *testing.T) { + assert := assert.New(t) + + input := []byte(`["1234",22]`) + + // naive is float,string + var p1 []interface{} + err := json.Unmarshal(input, &p1) + if assert.Nil(err) { + v, ok := p1[0].(string) + if assert.True(ok, "%#v", p1[0]) { + assert.EqualValues("1234", v) + } + h, ok := p1[1].(float64) + if assert.True(ok, "%#v", p1[1]) { + assert.EqualValues(22, h) + } + } + + // preloading map with values helps here (unlike map - p2 above) + tmp := 0 + p2 := []interface{}{&cmn.HexBytes{}, &tmp} + err = json.Unmarshal(input, &p2) + if assert.Nil(err) { + v, ok := p2[0].(*cmn.HexBytes) + if assert.True(ok, "%#v", p2[0]) { + assert.EqualValues([]byte{0x12, 0x34}, *v) + } + h, ok := p2[1].(*int) + if assert.True(ok, "%#v", p2[1]) { + assert.EqualValues(22, *h) + } + } +} + +func TestParseJSONRPC(t *testing.T) { + assert := assert.New(t) + + demo := func(height int, name string) {} + call := NewRPCFunc(demo, "height,name") + cdc := amino.NewCodec() + + cases := []struct { + raw string + height int64 + name string + fail bool + }{ + // should parse + {`["7", "flew"]`, 7, "flew", false}, + {`{"name": "john", "height": "22"}`, 22, "john", false}, + // defaults + {`{"name": "solo", "unused": "stuff"}`, 0, "solo", false}, + // should fail - wrong types/length + {`["flew", 7]`, 0, "", true}, + {`[7,"flew",100]`, 0, "", true}, + {`{"name": -12, "height": "fred"}`, 0, "", true}, + } + for idx, tc := range cases { + i := strconv.Itoa(idx) + data := []byte(tc.raw) + vals, err := jsonParamsToArgs(call, cdc, data, 0) + if tc.fail { + assert.NotNil(err, i) + } else { + assert.Nil(err, "%s: %+v", i, err) + if assert.Equal(2, len(vals), i) { + assert.Equal(tc.height, vals[0].Int(), i) + assert.Equal(tc.name, vals[1].String(), i) + } + } + + } +} + +func TestParseURI(t *testing.T) { + + demo := func(height int, name string) {} + call := NewRPCFunc(demo, "height,name") + cdc := amino.NewCodec() + + cases := []struct { + raw []string + height int64 + name string + fail bool + }{ + // can parse numbers unquoted and strings quoted + {[]string{"7", `"flew"`}, 7, "flew", false}, + {[]string{"22", `"john"`}, 22, "john", false}, + {[]string{"-10", `"bob"`}, -10, "bob", false}, + // can parse numbers quoted, too + {[]string{`"7"`, `"flew"`}, 7, "flew", false}, + {[]string{`"-10"`, `"bob"`}, -10, "bob", false}, + // cant parse strings uquoted + {[]string{`"-10"`, `bob`}, -10, "bob", true}, + } + for idx, tc := range cases { + i := strconv.Itoa(idx) + // data := []byte(tc.raw) + url := fmt.Sprintf( + "test.com/method?height=%v&name=%v", + tc.raw[0], tc.raw[1]) + req, err := http.NewRequest("GET", url, nil) + assert.NoError(t, err) + vals, err := httpParamsToArgs(call, cdc, req) + if tc.fail { + assert.NotNil(t, err, i) + } else { + assert.Nil(t, err, "%s: %+v", i, err) + if assert.Equal(t, 2, len(vals), i) { + assert.Equal(t, tc.height, vals[0].Int(), i) + assert.Equal(t, tc.name, vals[1].String(), i) + } + } + + } +} diff --git a/rpc/lib/types/types.go b/rpc/lib/types/types.go new file mode 100644 index 000000000..2a291890f --- /dev/null +++ b/rpc/lib/types/types.go @@ -0,0 +1,272 @@ +package rpctypes + +import ( + "context" + "encoding/json" + "fmt" + "reflect" + "strings" + + "github.com/pkg/errors" + + amino "github.com/MinterTeam/go-amino" + + tmpubsub "github.com/tendermint/tendermint/libs/pubsub" +) + +// a wrapper to emulate a sum type: jsonrpcid = string | int +// TODO: refactor when Go 2.0 arrives https://github.com/golang/go/issues/19412 +type jsonrpcid interface { + isJSONRPCID() +} + +// JSONRPCStringID a wrapper for JSON-RPC string IDs +type JSONRPCStringID string + +func (JSONRPCStringID) isJSONRPCID() {} + +// JSONRPCIntID a wrapper for JSON-RPC integer IDs +type JSONRPCIntID int + +func (JSONRPCIntID) isJSONRPCID() {} + +func idFromInterface(idInterface interface{}) (jsonrpcid, error) { + switch id := idInterface.(type) { + case string: + return JSONRPCStringID(id), nil + case float64: + // json.Unmarshal uses float64 for all numbers + // (https://golang.org/pkg/encoding/json/#Unmarshal), + // but the JSONRPC2.0 spec says the id SHOULD NOT contain + // decimals - so we truncate the decimals here. + return JSONRPCIntID(int(id)), nil + default: + typ := reflect.TypeOf(id) + return nil, fmt.Errorf("JSON-RPC ID (%v) is of unknown type (%v)", id, typ) + } +} + +//---------------------------------------- +// REQUEST + +type RPCRequest struct { + JSONRPC string `json:"jsonrpc"` + ID jsonrpcid `json:"id"` + Method string `json:"method"` + Params json.RawMessage `json:"params"` // must be map[string]interface{} or []interface{} +} + +// UnmarshalJSON custom JSON unmarshalling due to jsonrpcid being string or int +func (request *RPCRequest) UnmarshalJSON(data []byte) error { + unsafeReq := &struct { + JSONRPC string `json:"jsonrpc"` + ID interface{} `json:"id"` + Method string `json:"method"` + Params json.RawMessage `json:"params"` // must be map[string]interface{} or []interface{} + }{} + err := json.Unmarshal(data, &unsafeReq) + if err != nil { + return err + } + request.JSONRPC = unsafeReq.JSONRPC + request.Method = unsafeReq.Method + request.Params = unsafeReq.Params + if unsafeReq.ID == nil { + return nil + } + id, err := idFromInterface(unsafeReq.ID) + if err != nil { + return err + } + request.ID = id + return nil +} + +func NewRPCRequest(id jsonrpcid, method string, params json.RawMessage) RPCRequest { + return RPCRequest{ + JSONRPC: "2.0", + ID: id, + Method: method, + Params: params, + } +} + +func (req RPCRequest) String() string { + return fmt.Sprintf("[%s %s]", req.ID, req.Method) +} + +func MapToRequest(cdc *amino.Codec, id jsonrpcid, method string, params map[string]interface{}) (RPCRequest, error) { + var params_ = make(map[string]json.RawMessage, len(params)) + for name, value := range params { + valueJSON, err := cdc.MarshalJSON(value) + if err != nil { + return RPCRequest{}, err + } + params_[name] = valueJSON + } + payload, err := json.Marshal(params_) // NOTE: Amino doesn't handle maps yet. + if err != nil { + return RPCRequest{}, err + } + request := NewRPCRequest(id, method, payload) + return request, nil +} + +func ArrayToRequest(cdc *amino.Codec, id jsonrpcid, method string, params []interface{}) (RPCRequest, error) { + var params_ = make([]json.RawMessage, len(params)) + for i, value := range params { + valueJSON, err := cdc.MarshalJSON(value) + if err != nil { + return RPCRequest{}, err + } + params_[i] = valueJSON + } + payload, err := json.Marshal(params_) // NOTE: Amino doesn't handle maps yet. + if err != nil { + return RPCRequest{}, err + } + request := NewRPCRequest(id, method, payload) + return request, nil +} + +//---------------------------------------- +// RESPONSE + +type RPCError struct { + Code int `json:"code"` + Message string `json:"message"` + Data string `json:"data,omitempty"` +} + +func (err RPCError) Error() string { + const baseFormat = "RPC error %v - %s" + if err.Data != "" { + return fmt.Sprintf(baseFormat+": %s", err.Code, err.Message, err.Data) + } + return fmt.Sprintf(baseFormat, err.Code, err.Message) +} + +type RPCResponse struct { + JSONRPC string `json:"jsonrpc"` + ID jsonrpcid `json:"id"` + Result json.RawMessage `json:"result,omitempty"` + Error *RPCError `json:"error,omitempty"` +} + +// UnmarshalJSON custom JSON unmarshalling due to jsonrpcid being string or int +func (response *RPCResponse) UnmarshalJSON(data []byte) error { + unsafeResp := &struct { + JSONRPC string `json:"jsonrpc"` + ID interface{} `json:"id"` + Result json.RawMessage `json:"result,omitempty"` + Error *RPCError `json:"error,omitempty"` + }{} + err := json.Unmarshal(data, &unsafeResp) + if err != nil { + return err + } + response.JSONRPC = unsafeResp.JSONRPC + response.Error = unsafeResp.Error + response.Result = unsafeResp.Result + if unsafeResp.ID == nil { + return nil + } + id, err := idFromInterface(unsafeResp.ID) + if err != nil { + return err + } + response.ID = id + return nil +} + +func NewRPCSuccessResponse(cdc *amino.Codec, id jsonrpcid, res interface{}) RPCResponse { + var rawMsg json.RawMessage + + if res != nil { + var js []byte + js, err := cdc.MarshalJSON(res) + if err != nil { + return RPCInternalError(id, errors.Wrap(err, "Error marshalling response")) + } + rawMsg = json.RawMessage(js) + } + + return RPCResponse{JSONRPC: "2.0", ID: id, Result: rawMsg} +} + +func NewRPCErrorResponse(id jsonrpcid, code int, msg string, data string) RPCResponse { + return RPCResponse{ + JSONRPC: "2.0", + ID: id, + Error: &RPCError{Code: code, Message: msg, Data: data}, + } +} + +func (resp RPCResponse) String() string { + if resp.Error == nil { + return fmt.Sprintf("[%s %v]", resp.ID, resp.Result) + } + return fmt.Sprintf("[%s %s]", resp.ID, resp.Error) +} + +func RPCParseError(id jsonrpcid, err error) RPCResponse { + return NewRPCErrorResponse(id, -32700, "Parse error. Invalid JSON", err.Error()) +} + +func RPCInvalidRequestError(id jsonrpcid, err error) RPCResponse { + return NewRPCErrorResponse(id, -32600, "Invalid Request", err.Error()) +} + +func RPCMethodNotFoundError(id jsonrpcid) RPCResponse { + return NewRPCErrorResponse(id, -32601, "Method not found", "") +} + +func RPCInvalidParamsError(id jsonrpcid, err error) RPCResponse { + return NewRPCErrorResponse(id, -32602, "Invalid params", err.Error()) +} + +func RPCInternalError(id jsonrpcid, err error) RPCResponse { + return NewRPCErrorResponse(id, -32603, "Internal error", err.Error()) +} + +func RPCServerError(id jsonrpcid, err error) RPCResponse { + return NewRPCErrorResponse(id, -32000, "Server error", err.Error()) +} + +//---------------------------------------- + +// *wsConnection implements this interface. +type WSRPCConnection interface { + GetRemoteAddr() string + WriteRPCResponse(resp RPCResponse) + TryWriteRPCResponse(resp RPCResponse) bool + GetEventSubscriber() EventSubscriber + Codec() *amino.Codec +} + +// EventSubscriber mirros tendermint/tendermint/types.EventBusSubscriber +type EventSubscriber interface { + Subscribe(ctx context.Context, subscriber string, query tmpubsub.Query, out chan<- interface{}) error + Unsubscribe(ctx context.Context, subscriber string, query tmpubsub.Query) error + UnsubscribeAll(ctx context.Context, subscriber string) error +} + +// websocket-only RPCFuncs take this as the first parameter. +type WSRPCContext struct { + Request RPCRequest + WSRPCConnection +} + +//---------------------------------------- +// SOCKETS +// +// Determine if its a unix or tcp socket. +// If tcp, must specify the port; `0.0.0.0` will return incorrectly as "unix" since there's no port +// TODO: deprecate +func SocketType(listenAddr string) string { + socketType := "unix" + if len(strings.Split(listenAddr, ":")) >= 2 { + socketType = "tcp" + } + return socketType +} diff --git a/rpc/lib/types/types_test.go b/rpc/lib/types/types_test.go new file mode 100644 index 000000000..3e8851326 --- /dev/null +++ b/rpc/lib/types/types_test.go @@ -0,0 +1,84 @@ +package rpctypes + +import ( + "encoding/json" + "testing" + + "fmt" + + "github.com/pkg/errors" + "github.com/stretchr/testify/assert" + "github.com/tendermint/go-amino" +) + +type SampleResult struct { + Value string +} + +type responseTest struct { + id jsonrpcid + expected string +} + +var responseTests = []responseTest{ + {JSONRPCStringID("1"), `"1"`}, + {JSONRPCStringID("alphabet"), `"alphabet"`}, + {JSONRPCStringID(""), `""`}, + {JSONRPCStringID("àáâ"), `"àáâ"`}, + {JSONRPCIntID(-1), "-1"}, + {JSONRPCIntID(0), "0"}, + {JSONRPCIntID(1), "1"}, + {JSONRPCIntID(100), "100"}, +} + +func TestResponses(t *testing.T) { + assert := assert.New(t) + cdc := amino.NewCodec() + for _, tt := range responseTests { + jsonid := tt.id + a := NewRPCSuccessResponse(cdc, jsonid, &SampleResult{"hello"}) + b, _ := json.Marshal(a) + s := fmt.Sprintf(`{"jsonrpc":"2.0","id":%v,"result":{"Value":"hello"}}`, tt.expected) + assert.Equal(string(s), string(b)) + + d := RPCParseError(jsonid, errors.New("Hello world")) + e, _ := json.Marshal(d) + f := fmt.Sprintf(`{"jsonrpc":"2.0","id":%v,"error":{"code":-32700,"message":"Parse error. Invalid JSON","data":"Hello world"}}`, tt.expected) + assert.Equal(string(f), string(e)) + + g := RPCMethodNotFoundError(jsonid) + h, _ := json.Marshal(g) + i := fmt.Sprintf(`{"jsonrpc":"2.0","id":%v,"error":{"code":-32601,"message":"Method not found"}}`, tt.expected) + assert.Equal(string(h), string(i)) + } +} + +func TestUnmarshallResponses(t *testing.T) { + assert := assert.New(t) + cdc := amino.NewCodec() + for _, tt := range responseTests { + response := &RPCResponse{} + err := json.Unmarshal([]byte(fmt.Sprintf(`{"jsonrpc":"2.0","id":%v,"result":{"Value":"hello"}}`, tt.expected)), response) + assert.Nil(err) + a := NewRPCSuccessResponse(cdc, tt.id, &SampleResult{"hello"}) + assert.Equal(*response, a) + } + response := &RPCResponse{} + err := json.Unmarshal([]byte(`{"jsonrpc":"2.0","id":true,"result":{"Value":"hello"}}`), response) + assert.NotNil(err) +} + +func TestRPCError(t *testing.T) { + assert.Equal(t, "RPC error 12 - Badness: One worse than a code 11", + fmt.Sprintf("%v", &RPCError{ + Code: 12, + Message: "Badness", + Data: "One worse than a code 11", + })) + + assert.Equal(t, "RPC error 12 - Badness", + fmt.Sprintf("%v", &RPCError{ + Code: 12, + Message: "Badness", + })) +} diff --git a/rpc/test/helpers.go b/rpc/test/helpers.go new file mode 100644 index 000000000..e68ec1490 --- /dev/null +++ b/rpc/test/helpers.go @@ -0,0 +1,138 @@ +package rpctest + +import ( + "context" + "fmt" + "os" + "path/filepath" + "strings" + "time" + + "github.com/tendermint/tendermint/libs/log" + + abci "github.com/tendermint/tendermint/abci/types" + cmn "github.com/tendermint/tendermint/libs/common" + + cfg "github.com/tendermint/tendermint/config" + nm "github.com/tendermint/tendermint/node" + "github.com/tendermint/tendermint/p2p" + "github.com/tendermint/tendermint/privval" + "github.com/tendermint/tendermint/proxy" + ctypes "github.com/tendermint/tendermint/rpc/core/types" + core_grpc "github.com/tendermint/tendermint/rpc/grpc" + rpcclient "github.com/tendermint/tendermint/rpc/lib/client" +) + +var globalConfig *cfg.Config + +func waitForRPC() { + laddr := GetConfig().RPC.ListenAddress + client := rpcclient.NewJSONRPCClient(laddr) + ctypes.RegisterAmino(client.Codec()) + result := new(ctypes.ResultStatus) + for { + _, err := client.Call("status", map[string]interface{}{}, result) + if err == nil { + return + } else { + fmt.Println("error", err) + time.Sleep(time.Millisecond) + } + } +} + +func waitForGRPC() { + client := GetGRPCClient() + for { + _, err := client.Ping(context.Background(), &core_grpc.RequestPing{}) + 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, grpc := makeAddrs() + globalConfig.P2P.ListenAddress = tm + globalConfig.RPC.ListenAddress = rpc + globalConfig.RPC.CORSAllowedOrigins = []string{"https://tendermint.com/"} + globalConfig.RPC.GRPCListenAddress = grpc + globalConfig.TxIndex.IndexTags = "app.creator,tx.height" // see kvstore application + } + return globalConfig +} + +func GetGRPCClient() core_grpc.BroadcastAPIClient { + grpcAddr := globalConfig.RPC.GRPCListenAddress + return core_grpc.StartGRPCClient(grpcAddr) +} + +// StartTendermint starts a test tendermint server in a go routine and returns when it is initialized +func StartTendermint(app abci.Application) *nm.Node { + node := NewTendermint(app) + err := node.Start() + if err != nil { + panic(err) + } + + // wait for rpc + waitForRPC() + waitForGRPC() + + fmt.Println("Tendermint running!") + + return node +} + +// NewTendermint creates a new tendermint server and sleeps forever +func NewTendermint(app abci.Application) *nm.Node { + // Create & start node + config := GetConfig() + logger := log.NewTMLogger(log.NewSyncWriter(os.Stdout)) + logger = log.NewFilter(logger, log.AllowError()) + pvFile := config.PrivValidatorFile() + pv := privval.LoadOrGenFilePV(pvFile) + papp := proxy.NewLocalClientCreator(app) + nodeKey, err := p2p.LoadOrGenNodeKey(config.NodeKeyFile()) + if err != nil { + panic(err) + } + node, err := nm.NewNode(config, pv, nodeKey, papp, + nm.DefaultGenesisDocProviderFunc(config), + nm.DefaultDBProvider, + nm.DefaultMetricsProvider(config.Instrumentation), + logger) + if err != nil { + panic(err) + } + return node +} From 03a5f7554cf5685c19c0952000a1befd2b0b08e3 Mon Sep 17 00:00:00 2001 From: Daniil Lashin Date: Mon, 3 Dec 2018 17:51:38 +0300 Subject: [PATCH 22/49] Fix transaction api --- api/transaction.go | 40 +++++++++++++++++++++++++++++++++++++++- api/transactions.go | 10 ++++++++-- 2 files changed, 47 insertions(+), 3 deletions(-) diff --git a/api/transaction.go b/api/transaction.go index b397f9a3e..2e77a456e 100644 --- a/api/transaction.go +++ b/api/transaction.go @@ -31,6 +31,11 @@ func Transaction(hash []byte) (*TransactionResponse, error) { } } + data, err := encodeTxData(decodedTx) + if err != nil { + return nil, err + } + return &TransactionResponse{ Hash: common.HexBytes(tx.Tx.Hash()), RawTx: fmt.Sprintf("%x", []byte(tx.Tx)), @@ -42,10 +47,43 @@ func Transaction(hash []byte) (*TransactionResponse, error) { GasCoin: decodedTx.GasCoin, GasUsed: tx.TxResult.GasUsed, Type: decodedTx.Type, - Data: decodedTx.GetDecodedData(), + Data: data, Payload: decodedTx.Payload, Tags: tags, Code: tx.TxResult.Code, Log: tx.TxResult.Log, }, nil } + +func encodeTxData(decodedTx *transaction.Transaction) ([]byte, error) { + switch decodedTx.Type { + case transaction.TypeSend: + return cdc.MarshalJSON(decodedTx.GetDecodedData().(transaction.SendData)) + case transaction.TypeRedeemCheck: + return cdc.MarshalJSON(decodedTx.GetDecodedData().(transaction.RedeemCheckData)) + case transaction.TypeSellCoin: + return cdc.MarshalJSON(decodedTx.GetDecodedData().(transaction.SellCoinData)) + case transaction.TypeSellAllCoin: + return cdc.MarshalJSON(decodedTx.GetDecodedData().(transaction.SellAllCoinData)) + case transaction.TypeBuyCoin: + return cdc.MarshalJSON(decodedTx.GetDecodedData().(transaction.BuyCoinData)) + case transaction.TypeCreateCoin: + return cdc.MarshalJSON(decodedTx.GetDecodedData().(transaction.CreateCoinData)) + case transaction.TypeDeclareCandidacy: + return cdc.MarshalJSON(decodedTx.GetDecodedData().(transaction.DeclareCandidacyData)) + case transaction.TypeDelegate: + return cdc.MarshalJSON(decodedTx.GetDecodedData().(transaction.DelegateData)) + case transaction.TypeSetCandidateOnline: + return cdc.MarshalJSON(decodedTx.GetDecodedData().(transaction.SetCandidateOnData)) + case transaction.TypeSetCandidateOffline: + return cdc.MarshalJSON(decodedTx.GetDecodedData().(transaction.SetCandidateOffData)) + case transaction.TypeUnbond: + return cdc.MarshalJSON(decodedTx.GetDecodedData().(transaction.UnbondData)) + case transaction.TypeCreateMultisig: + return cdc.MarshalJSON(decodedTx.GetDecodedData().(transaction.CreateMultisigData)) + case transaction.TypeMultisend: + return cdc.MarshalJSON(decodedTx.GetDecodedData().(transaction.MultisendData)) + } + + return nil, errors.New("unknown tx type") +} diff --git a/api/transactions.go b/api/transactions.go index c092e72c4..471be567f 100644 --- a/api/transactions.go +++ b/api/transactions.go @@ -1,6 +1,7 @@ package api import ( + "encoding/json" "fmt" "github.com/MinterTeam/minter-go-node/core/transaction" "github.com/MinterTeam/minter-go-node/core/types" @@ -20,7 +21,7 @@ type TransactionResponse struct { GasCoin types.CoinSymbol `json:"gas_coin"` GasUsed int64 `json:"gas_used"` Type byte `json:"type"` - Data transaction.Data `json:"data"` + Data json.RawMessage `json:"data"` Payload []byte `json:"payload"` Tags map[string]string `json:"tags"` Code uint32 `json:"code,omitempty"` @@ -54,6 +55,11 @@ func Transactions(query string) (*[]TransactionResponse, error) { } } + data, err := encodeTxData(decodedTx) + if err != nil { + return nil, err + } + result[i] = TransactionResponse{ Hash: common.HexBytes(tx.Tx.Hash()), RawTx: fmt.Sprintf("%x", []byte(tx.Tx)), @@ -65,7 +71,7 @@ func Transactions(query string) (*[]TransactionResponse, error) { GasCoin: decodedTx.GasCoin, GasUsed: tx.TxResult.GasUsed, Type: decodedTx.Type, - Data: decodedTx.GetDecodedData(), + Data: data, Payload: decodedTx.Payload, Tags: tags, Code: tx.TxResult.Code, From a1dfc04c995058aa067a26bca7b2c8d78d7476a0 Mon Sep 17 00:00:00 2001 From: Daniil Lashin Date: Mon, 3 Dec 2018 17:52:54 +0300 Subject: [PATCH 23/49] Fix block api --- api/block.go | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/api/block.go b/api/block.go index ba8cae1ba..08f051185 100644 --- a/api/block.go +++ b/api/block.go @@ -1,6 +1,7 @@ package api import ( + "encoding/json" "fmt" "github.com/MinterTeam/minter-go-node/core/rewards" "github.com/MinterTeam/minter-go-node/core/transaction" @@ -38,7 +39,7 @@ type BlockTransactionResponse struct { Nonce uint64 `json:"nonce"` GasPrice *big.Int `json:"gas_price"` Type byte `json:"type"` - Data transaction.Data `json:"data"` + Data json.RawMessage `json:"data"` Payload []byte `json:"payload"` ServiceData []byte `json:"service_data"` Gas int64 `json:"gas"` @@ -72,6 +73,11 @@ func Block(height int64) (*BlockResponse, error) { } } + data, err := encodeTxData(tx) + if err != nil { + return nil, err + } + txs[i] = BlockTransactionResponse{ Hash: fmt.Sprintf("Mt%x", rawTx.Hash()), RawTx: fmt.Sprintf("%x", []byte(rawTx)), @@ -79,7 +85,7 @@ func Block(height int64) (*BlockResponse, error) { Nonce: tx.Nonce, GasPrice: tx.GasPrice, Type: tx.Type, - Data: tx.GetDecodedData(), + Data: data, Payload: tx.Payload, ServiceData: tx.ServiceData, Gas: tx.Gas(), From b72dc6ec43ca3e2ea6a2aa1261455ca88f59b03a Mon Sep 17 00:00:00 2001 From: Daniil Lashin Date: Mon, 3 Dec 2018 18:08:50 +0300 Subject: [PATCH 24/49] Fix API and update docs --- api/api.go | 2 +- api/estimate_tx_commission.go | 19 ++- docs/api.rst | 245 ++++++++++++++-------------------- 3 files changed, 112 insertions(+), 154 deletions(-) diff --git a/api/api.go b/api/api.go index 246e438b6..5ee603b68 100644 --- a/api/api.go +++ b/api/api.go @@ -47,7 +47,7 @@ var Routes = map[string]*rpcserver.RPCFunc{ "coin_info": rpcserver.NewRPCFunc(CoinInfo, "symbol,height"), "estimate_coin_sell": rpcserver.NewRPCFunc(EstimateCoinSell, "coin_to_sell,coin_to_buy,value_to_sell,height"), "estimate_coin_buy": rpcserver.NewRPCFunc(EstimateCoinBuy, "coin_to_sell,coin_to_buy,value_to_buy,height"), - "estimate_tx_commission": rpcserver.NewRPCFunc(EstimateTxCommission, "tx"), + "estimate_tx_commission": rpcserver.NewRPCFunc(EstimateTxCommission, "tx,height"), "unconfirmed_txs": rpcserver.NewRPCFunc(UnconfirmedTxs, "limit"), } diff --git a/api/estimate_tx_commission.go b/api/estimate_tx_commission.go index ac22cafbf..1b4fe4d78 100644 --- a/api/estimate_tx_commission.go +++ b/api/estimate_tx_commission.go @@ -8,23 +8,26 @@ import ( "math/big" ) -func EstimateTxCommission(rawTx []byte, height int) (*big.Int, error) { +type TxCommissionResponse struct { + Commission *big.Int `json:"commission"` +} + +func EstimateTxCommission(tx []byte, height int) (*TxCommissionResponse, error) { cState, err := GetStateForHeight(height) if err != nil { return nil, err } - tx, err := transaction.DecodeFromBytes(rawTx) + decodedTx, err := transaction.DecodeFromBytes(tx) if err != nil { return nil, err } - commissionInBaseCoin := big.NewInt(0).Mul(tx.GasPrice, big.NewInt(tx.Gas())) - commissionInBaseCoin.Mul(commissionInBaseCoin, transaction.CommissionMultiplier) + commissionInBaseCoin := decodedTx.CommissionInBaseCoin() commission := big.NewInt(0).Set(commissionInBaseCoin) - if !tx.GasCoin.IsBaseCoin() { - coin := cState.GetStateCoin(tx.GasCoin) + if !decodedTx.GasCoin.IsBaseCoin() { + coin := cState.GetStateCoin(decodedTx.GasCoin) if coin.ReserveBalance().Cmp(commissionInBaseCoin) < 0 { return nil, errors.New(fmt.Sprintf("Coin reserve balance is not sufficient for transaction. Has: %s, required %s", coin.ReserveBalance().String(), commissionInBaseCoin.String())) @@ -33,5 +36,7 @@ func EstimateTxCommission(rawTx []byte, height int) (*big.Int, error) { commission = formula.CalculateSaleAmount(coin.Volume(), coin.ReserveBalance(), coin.Data().Crr, commissionInBaseCoin) } - return commission, nil + return &TxCommissionResponse{ + Commission: commission, + }, nil } diff --git a/docs/api.rst b/docs/api.rst index 12bc1626a..5a301bd23 100755 --- a/docs/api.rst +++ b/docs/api.rst @@ -170,8 +170,8 @@ Returns the balance of given account and the number of outgoing transaction. -**Result->balance**: Map of balances. CoinSymbol => Balance (in pips). -**Result->transaction_count**: Count of transactions sent from the account. +| **Result->balance**: Map of balances. CoinSymbol => Balance (in pips). +| **Result->transaction_count**: Count of transactions sent from the account. Send transaction ^^^^^^^^^^^^^^^^ @@ -185,12 +185,17 @@ Sends transaction to the Minter Network. .. code-block:: json { + "jsonrpc": "2.0", + "id": "", + "result": { "code": 0, - "result": { - "hash": "Mtfd5c3ecad1e8333564cf6e3f968578b9db5acea3" - } + "data": "", + "log": "", + "hash": "C6C6B5008AF8077FB0CE817DDB79268D1C66B6B353AF76778CA5A264A80069DB" + } } + **Result**: Transaction hash. Transaction @@ -203,33 +208,35 @@ Transaction .. code-block:: json { - "code": 0, - "result": { - "hash": "B829EE45734800273ACCCFA70BC96BE8D858E521", - "raw_tx": "f88682c8e8018a4d4e540000000000000001abea8a4d4e5400000000000000941a8e2cd08a2938b6412cc65aed449154577731e089056bc75e2d63100000808001b845f8431ba050141c66539362464496d1393b8a9468623f37dced4cf3bac8bfc5d576fd5e1fa00b37ee7103d2647bd7fc1a72c03df2a40cc38c9cc92771614f76eabf7be1cc79", - "height": 234218, - "index": 0, - "from": "Mxfe60014a6e9ac91618f5d1cab3fd58cded61ee99", - "nonce": 51432, - "gas_price": 1, - "gas_coin": "MNT", - "gas_used": 10, - "type": 1, - "data": { - "coin": "MNT", - "to": "Mx1a8e2cd08a2938b6412cc65aed449154577731e0", - "value": "100000000000000000000" - }, - "payload": "", - "tags": { - "tx.coin": "MNT", - "tx.from": "fe60014a6e9ac91618f5d1cab3fd58cded61ee99", - "tx.to": "1a8e2cd08a2938b6412cc65aed449154577731e0", - "tx.type": "01" - } + "jsonrpc": "2.0", + "id": "", + "result": { + "hash": "C6C6B5008AF8077FB0CE817DDB79268D1C66B6B353AF76778CA5A264A80069DB", + "raw_tx": "f88701018a4d4e540000000000000001aae98a4d4e540000000000000094ee81347211c72524338f9680072af9074433314688a688906bd8b0000084546573748001b845f8431ba098fd9402b0af434f461eecdad89908655c779fb394b7624a0c37198f931f27a1a075e73a04f81e2204d88826ac851b2b3da359e4a9a16ac6c17e992fa0a3de0c48", + "height": "387", + "index": 0, + "from": "Mxee81347211c72524338f9680072af90744333146", + "nonce": "1", + "gas_price": "1", + "gas_coin": "MNT", + "gas_used": "18", + "type": 1, + "data": { + "coin": "MNT", + "to": "Mxee81347211c72524338f9680072af90744333146", + "value": "12000000000000000000" + }, + "payload": "VGVzdA==", + "tags": { + "tx.coin": "MNT", + "tx.type": "01", + "tx.from": "ee81347211c72524338f9680072af90744333146", + "tx.to": "ee81347211c72524338f9680072af90744333146" } + } } + Block ^^^^^ @@ -237,131 +244,70 @@ Returns block data at given height. .. code-block:: bash - curl -s 'localhost:8841/block/{height}' + curl -s 'localhost:8841/block?height={height}' .. code-block:: json { - "code": 0, + "jsonrpc": "2.0", + "id": "", "result": { - "hash": "6B4F84E0C801EE01B4EA1AEC34B0A0249E4EB3FF", - "height": 94594, - "time": "2018-08-29T10:12:52.791097555Z", - "num_txs": 1, - "total_txs": 5515, + "hash": "F4D2F2DF68B20275B832B2D1859308509C373523689259CABD17AFC777C0B014", + "height": "387", + "time": "2018-12-03T14:39:41.364276Z", + "num_txs": "1", + "total_txs": "1", "transactions": [ { - "hash": "B829EE45734800273ACCCFA70BC96BE8D858E521", - "raw_tx": "f88682c8e8018a4d4e540000000000000001abea8a4d4e5400000000000000941a8e2cd08a2938b6412cc65aed449154577731e089056bc75e2d63100000808001b845f8431ba050141c66539362464496d1393b8a9468623f37dced4cf3bac8bfc5d576fd5e1fa00b37ee7103d2647bd7fc1a72c03df2a40cc38c9cc92771614f76eabf7be1cc79", - "height": 94594, - "index": 0, - "from": "Mxfe60014a6e9ac91618f5d1cab3fd58cded61ee99", - "nonce": 51432, - "gas_price": 1, - "gas_coin": "MNT", - "gas_used": 10, + "hash": "Mtc6c6b5008af8077fb0ce817ddb79268d1c66b6b353af76778ca5a264a80069db", + "raw_tx": "f88701018a4d4e540000000000000001aae98a4d4e540000000000000094ee81347211c72524338f9680072af9074433314688a688906bd8b0000084546573748001b845f8431ba098fd9402b0af434f461eecdad89908655c779fb394b7624a0c37198f931f27a1a075e73a04f81e2204d88826ac851b2b3da359e4a9a16ac6c17e992fa0a3de0c48", + "from": "Mxee81347211c72524338f9680072af90744333146", + "nonce": "1", + "gas_price": "1", "type": 1, "data": { - "coin": "MNT", - "to": "Mx1a8e2cd08a2938b6412cc65aed449154577731e0", - "value": "100000000000000000000" + "coin": "MNT", + "to": "Mxee81347211c72524338f9680072af90744333146", + "value": "12000000000000000000" }, - "payload": "", + "payload": "VGVzdA==", + "service_data": "", + "gas": "18", + "gas_coin": "MNT", + "gas_used": "18", "tags": { - "tx.coin": "MNT", - "tx.from": "fe60014a6e9ac91618f5d1cab3fd58cded61ee99", - "tx.to": "1a8e2cd08a2938b6412cc65aed449154577731e0", - "tx.type": "01" + "tx.type": "01", + "tx.from": "ee81347211c72524338f9680072af90744333146", + "tx.to": "ee81347211c72524338f9680072af90744333146", + "tx.coin": "MNT" } } ], "precommits": [ { - "validator_address": "0D1A38E170F4BC84CBA505E041AF0A656FEF7CCE", - "validator_index": "0", - "height": "94593", - "round": "0", - "timestamp": "2018-08-29T10:12:47.480971248Z", "type": 2, - "block_id": { - "hash": "CCC196AE488111387594258B4F5B417B6DF6F01E", - "parts": { - "total": "1", - "hash": "6C8A070EBDD7218547617CD2E0894E031B815B95" - } - }, - "signature": "+tNZnoPJnQNpanlK90YEb11GnGP20wGzrrqX7Wzf729KhZBhOkK4zFZW0CnUfVHwYpu4nGVaJLOgy8G6VKCgCg==" - }, - { - "validator_address": "1B16468F89B8C36FE1AFC7F82F7251D4FC831530", - "validator_index": "1", - "height": "94593", + "height": "386", "round": "0", - "timestamp": "2018-08-29T10:12:47.494792759Z", - "type": 2, + "timestamp": "2018-12-03T14:39:41.364276Z", "block_id": { - "hash": "CCC196AE488111387594258B4F5B417B6DF6F01E", + "hash": "8348B85D729555F0FEC3258EE07B188A38702F5045C1C0E3F0200A59713AA32F", "parts": { "total": "1", - "hash": "6C8A070EBDD7218547617CD2E0894E031B815B95" + "hash": "5816E926E9006AF09D6E77A51A90051B54E2D6FF984A21BE4AAC39A0E0758678" } }, - "signature": "GofqbrNFZye3pQk8sDsuErFH4x4Z+bs7skQOeeTcNA+jSIoupo+NWM6SV/rePg6NVOSA3PHVkXG6MVO2xYfbCg==" - }, - { - "validator_address": "22794FF373BE0867ECCB8206BEB77E0AB6F4A198", - "validator_index": "2", - "height": "94593", - "round": "0", - "timestamp": "2018-08-29T10:12:47.465617407Z", - "type": 2, - "block_id": { - "hash": "CCC196AE488111387594258B4F5B417B6DF6F01E", - "parts": { - "total": "1", - "hash": "6C8A070EBDD7218547617CD2E0894E031B815B95" - } - }, - "signature": "19Xu5Y8UI4QwZc89HgC42G4dB8MaMn7ibph6R1iVo9YYwwTKN4NEOjbuvvl3VYl8k/8CBIhck45GtSq73xHiBA==" - }, - { - "validator_address": "36575649BE18934623E0CE226B8E60FB1D1E7163", - "validator_index": "3", - "height": "94593", - "round": "0", - "timestamp": "2018-08-29T10:12:47.488838407Z", - "type": 2, - "block_id": { - "hash": "CCC196AE488111387594258B4F5B417B6DF6F01E", - "parts": { - "total": "1", - "hash": "6C8A070EBDD7218547617CD2E0894E031B815B95" - } - }, - "signature": "5PE9BYgsnXGtzUeeUBqIwA/VTfunHC+gN1keQYeN220JSjXrI7qZguYm45+9dt79s/y6jc8S4XKRSDNvVI1DDg==" - }, - { - "validator_address": "6330D572B9670786E0603332C01E7D4C35653C4A", - "validator_index": "4", - "height": "94593", - "round": "0", - "timestamp": "2018-08-29T10:12:47.443544695Z", - "type": 2, - "block_id": { - "hash": "CCC196AE488111387594258B4F5B417B6DF6F01E", - "parts": { - "total": "1", - "hash": "6C8A070EBDD7218547617CD2E0894E031B815B95" - } - }, - "signature": "QTW+t2Yen2U04gO3T3CRG3nhAkmkVM1ucQRD4QZS5Pokwp8C9ykP3uEefXjgTznBd3x24+hkTHSUfOy/HY9CCw==" + "validator_address": "AB15A084DD592699812E9B22385C1959E7AEFFB8", + "validator_index": "0", + "signature": "V/9uM9ZVUgfh5NdZn0DS4xWubLqJiEAB+J1McBCKW0Kq2X25L4+6mSjEG/hSYNxLGtS+yV22bhxLcc2qvqoDAg==" } ], - "block_reward": "333000000000000000000" + "block_reward": "333000000000000000000", + "size": "1204" } } + Coin Info ^^^^^^^^^ @@ -371,18 +317,19 @@ Returns information about coin. .. code-block:: bash - curl -s 'localhost:8841/coinInfo/{symbol}' + curl -s 'localhost:8841/coin_info?symbol="{SYMBOL}"' .. code-block:: json { - "code": 0, + "jsonrpc": "2.0", + "id": "", "result": { - "name": "Stakeholder Coin", - "symbol": "SHSCOIN", - "volume": "1985888114702108355026636", - "crr": 50, - "reserve_balance": "394375160721239016660255" + "name": "TEST", + "symbol": "TESTCOIN", + "volume": "100000000000000000000", + "crr": "100", + "reserve_balance": "100000000000000000000" } } @@ -402,7 +349,7 @@ Return estimate of sell coin transaction .. code-block:: bash - curl -s 'localhost:8841/estimateCoinSell?coin_to_sell=MNT&value_to_sell=1000000000000000000&coin_to_buy=BLTCOIN' + curl -s 'localhost:8841/estimate_coin_sell?coin_to_sell="MNT"&coin_to_buy="TESTCOIN"&value_to_sell="1"' Request params: - **coin_to_sell** – coin to give @@ -412,13 +359,15 @@ Request params: .. code-block:: json { - "code": 0, - "result": { - "will_get": "29808848728151191", - "commission": "443372813245" - } + "jsonrpc": "2.0", + "id": "", + "result": { + "will_pay": "1", + "commission": "100000000000000000" + } } + **Result**: Amount of "to_coin" user should get. @@ -429,7 +378,7 @@ Return estimate of buy coin transaction .. code-block:: bash - curl -s 'localhost:8841/estimateCoinBuy?coin_to_sell=MNT&value_to_buy=1000000000000000000&coin_to_buy=BLTCOIN' + curl -s 'localhost:8841/estimate_coin_buy?coin_to_sell="MNT"&coin_to_buy="TESTCOIN"&value_to_buy="1"' Request params: - **coin_to_sell** – coin to give @@ -439,13 +388,15 @@ Request params: .. code-block:: json { - "code": 0, - "result": { - "will_pay": "29808848728151191", - "commission": "443372813245" - } + "jsonrpc": "2.0", + "id": "", + "result": { + "will_pay": "1", + "commission": "100000000000000000" + } } + **Result**: Amount of "to_coin" user should give. Estimate tx commission @@ -460,11 +411,13 @@ Return estimate of buy coin transaction .. code-block:: json { - "code": 0, + "jsonrpc": "2.0", + "id": "", "result": { - "commission": "10000000000000000" + "commission": "11000000000000000000" } } + **Result**: Commission in GasCoin. From f12ae39477c15b94b468037b209a2b5888bca458 Mon Sep 17 00:00:00 2001 From: Daniil Lashin Date: Mon, 3 Dec 2018 18:09:15 +0300 Subject: [PATCH 25/49] Fix typo --- docs/api.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api.rst b/docs/api.rst index 5a301bd23..e2b35b785 100755 --- a/docs/api.rst +++ b/docs/api.rst @@ -406,7 +406,7 @@ Return estimate of buy coin transaction .. code-block:: bash - curl -s 'localhost:8841/estimateTxCommission?tx={transaction}' + curl -s 'localhost:8841/estimate_tx_commission?tx={transaction}' .. code-block:: json From 58d84328a96fc88c033350c447235cb5d4b9b7bf Mon Sep 17 00:00:00 2001 From: Daniil Lashin Date: Mon, 3 Dec 2018 18:32:42 +0300 Subject: [PATCH 26/49] Fix tests --- core/transaction/buy_coin_test.go | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/core/transaction/buy_coin_test.go b/core/transaction/buy_coin_test.go index ba91c56b7..ab37e714f 100644 --- a/core/transaction/buy_coin_test.go +++ b/core/transaction/buy_coin_test.go @@ -2,14 +2,13 @@ package transaction import ( "bytes" + "github.com/MinterTeam/go-amino" "github.com/MinterTeam/minter-go-node/core/code" "github.com/MinterTeam/minter-go-node/core/state" "github.com/MinterTeam/minter-go-node/core/types" "github.com/MinterTeam/minter-go-node/crypto" "github.com/MinterTeam/minter-go-node/helpers" "github.com/MinterTeam/minter-go-node/rlp" - "github.com/tendermint/go-amino" - "github.com/tendermint/tendermint/crypto/encoding/amino" "github.com/tendermint/tendermint/libs/db" "math/big" "testing" @@ -19,10 +18,6 @@ var ( cdc = amino.NewCodec() ) -func init() { - cryptoAmino.RegisterAmino(cdc) -} - func getState() *state.StateDB { s, err := state.New(0, db.NewMemDB()) From 0aadd2429fe468b3acd20e31bb97cf52ae1dc1ce Mon Sep 17 00:00:00 2001 From: Daniil Lashin Date: Tue, 4 Dec 2018 12:19:33 +0300 Subject: [PATCH 27/49] Add some notes --- core/transaction/buy_coin.go | 11 +++++++++++ core/transaction/sell_all_coin.go | 1 + core/transaction/sell_coin.go | 15 +++++++++++++++ 3 files changed, 27 insertions(+) diff --git a/core/transaction/buy_coin.go b/core/transaction/buy_coin.go index dad513a90..6fb417a68 100644 --- a/core/transaction/buy_coin.go +++ b/core/transaction/buy_coin.go @@ -38,6 +38,12 @@ func (data BuyCoinData) TotalSpend(tx *Transaction, context *state.StateDB) (Tot if data.CoinToSell.IsBaseCoin() { coin := context.GetStateCoin(data.CoinToBuy).Data() + + if tx.GasCoin == data.CoinToBuy { + // commissionIncluded = true + // TODO: DO SMTH + } + value = formula.CalculatePurchaseAmount(coin.Volume, coin.ReserveBalance, coin.Crr, data.ValueToBuy) total.Add(data.CoinToSell, value) conversions = append(conversions, Conversion{ @@ -85,6 +91,11 @@ func (data BuyCoinData) TotalSpend(tx *Transaction, context *state.StateDB) (Tot types.GetBaseCoin())} } + if tx.GasCoin == data.CoinToBuy { + // commissionIncluded = true + // TODO: DO SMTH + } + value = formula.CalculateSaleAmount(coinFrom.Volume, coinFrom.ReserveBalance, coinFrom.Crr, baseCoinNeeded) total.Add(data.CoinToSell, value) conversions = append(conversions, Conversion{ diff --git a/core/transaction/sell_all_coin.go b/core/transaction/sell_all_coin.go index 115300fa2..4d6f60bb4 100644 --- a/core/transaction/sell_all_coin.go +++ b/core/transaction/sell_all_coin.go @@ -87,6 +87,7 @@ func (data SellAllCoinData) Run(tx *Transaction, context *state.StateDB, isCheck amountToSell := big.NewInt(0).Set(available) amountToSell.Sub(amountToSell, commission) + // TODO: move under errors if !isCheck { rewardPool.Add(rewardPool, commissionInBaseCoin) diff --git a/core/transaction/sell_coin.go b/core/transaction/sell_coin.go index f97c4ca25..9b3909d98 100644 --- a/core/transaction/sell_coin.go +++ b/core/transaction/sell_coin.go @@ -31,6 +31,11 @@ func (data SellCoinData) TotalSpend(tx *Transaction, context *state.StateDB) (To coin := context.GetStateCoin(data.CoinToBuy).Data() value = formula.CalculatePurchaseReturn(coin.Volume, coin.ReserveBalance, coin.Crr, data.ValueToSell) + if tx.GasCoin == data.CoinToBuy { + // commissionIncluded = true + // TODO: DO SMTH + } + total.Add(data.CoinToSell, data.ValueToSell) conversions = append(conversions, Conversion{ FromCoin: data.CoinToSell, @@ -59,6 +64,11 @@ func (data SellCoinData) TotalSpend(tx *Transaction, context *state.StateDB) (To rValue.Add(rValue, commissionInBaseCoin) } + if tx.GasCoin == data.CoinToBuy { + // commissionIncluded = true + // TODO: DO SMTH + } + total.Add(data.CoinToSell, valueToSell) conversions = append(conversions, Conversion{ FromCoin: data.CoinToSell, @@ -88,6 +98,11 @@ func (data SellCoinData) TotalSpend(tx *Transaction, context *state.StateDB) (To basecoinValue.Add(basecoinValue, commissionInBaseCoin) } + if tx.GasCoin == data.CoinToBuy { + // commissionIncluded = true + // TODO: DO SMTH + } + value = formula.CalculatePurchaseReturn(coinTo.Volume, coinTo.ReserveBalance, coinTo.Crr, basecoinValue) total.Add(data.CoinToSell, valueToSell) From 1e069c5444f32501858825a2c7e7383b385f58e3 Mon Sep 17 00:00:00 2001 From: Daniil Lashin Date: Tue, 4 Dec 2018 12:45:10 +0300 Subject: [PATCH 28/49] Limit max coins supply to 1,000,000,000,000,000 #171 --- CHANGELOG.md | 1 + core/code/code.go | 1 + core/transaction/buy_coin.go | 9 +++++++++ core/transaction/sell_coin.go | 14 ++++++++++++++ core/transaction/transaction.go | 12 ++++++++++++ 5 files changed, 37 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 10dbf568e..63aa01c0c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ BREAKING CHANGES - [api] Switch to RPC protocol - [core] Fix issue with incorrect coin conversion +- [core] Limit coins supply to 1,000,000,000,000,000 IMPROVEMENT diff --git a/core/code/code.go b/core/code/code.go index 525a02168..3021e7f01 100644 --- a/core/code/code.go +++ b/core/code/code.go @@ -12,6 +12,7 @@ const ( TxPayloadTooLarge uint32 = 109 TxServiceDataTooLarge uint32 = 110 InvalidMultisendData uint32 = 111 + CoinSupplyOverflow uint32 = 112 // coin creation CoinAlreadyExists uint32 = 201 diff --git a/core/transaction/buy_coin.go b/core/transaction/buy_coin.go index 6fb417a68..8c191dcdf 100644 --- a/core/transaction/buy_coin.go +++ b/core/transaction/buy_coin.go @@ -34,6 +34,15 @@ func (data BuyCoinData) TotalSpend(tx *Transaction, context *state.StateDB) (Tot commissionInBaseCoin := tx.CommissionInBaseCoin() commissionIncluded := false + coin := context.GetStateCoin(data.CoinToBuy) + + if err := CheckForCoinSupplyOverflow(coin.Volume(), data.ValueToBuy); err != nil { + return nil, nil, nil, &Response{ + Code: code.CoinSupplyOverflow, + Log: err.Error(), + } + } + var value *big.Int if data.CoinToSell.IsBaseCoin() { diff --git a/core/transaction/sell_coin.go b/core/transaction/sell_coin.go index 9b3909d98..7d21b5c47 100644 --- a/core/transaction/sell_coin.go +++ b/core/transaction/sell_coin.go @@ -36,6 +36,13 @@ func (data SellCoinData) TotalSpend(tx *Transaction, context *state.StateDB) (To // TODO: DO SMTH } + if err := CheckForCoinSupplyOverflow(coin.Volume, value); err != nil { + return nil, nil, nil, &Response{ + Code: code.CoinSupplyOverflow, + Log: err.Error(), + } + } + total.Add(data.CoinToSell, data.ValueToSell) conversions = append(conversions, Conversion{ FromCoin: data.CoinToSell, @@ -105,6 +112,13 @@ func (data SellCoinData) TotalSpend(tx *Transaction, context *state.StateDB) (To value = formula.CalculatePurchaseReturn(coinTo.Volume, coinTo.ReserveBalance, coinTo.Crr, basecoinValue) + if err := CheckForCoinSupplyOverflow(coinTo.Volume, value); err != nil { + return nil, nil, nil, &Response{ + Code: code.CoinSupplyOverflow, + Log: err.Error(), + } + } + total.Add(data.CoinToSell, valueToSell) conversions = append(conversions, Conversion{ diff --git a/core/transaction/transaction.go b/core/transaction/transaction.go index 55383a537..5efdae3b1 100644 --- a/core/transaction/transaction.go +++ b/core/transaction/transaction.go @@ -35,6 +35,7 @@ const ( var ( ErrInvalidSig = errors.New("invalid transaction v, r, s values") + MaxCoinSupply = big.NewInt(0).Exp(big.NewInt(1000), big.NewInt(5 + 18), nil) // 1,000,000,000,000,000 bips ) type Transaction struct { @@ -432,3 +433,14 @@ func DecodeFromBytes(buf []byte) (*Transaction, error) { return &tx, nil } + +func CheckForCoinSupplyOverflow(current *big.Int, delta *big.Int) error { + total := big.NewInt(0).Set(current) + total.Add(total, delta) + + if total.Cmp(MaxCoinSupply) != -1 { + return errors.New("Coin supply overflow") + } + + return nil +} From 12dd5712970316b064fbb0a35ea695b0624d8a29 Mon Sep 17 00:00:00 2001 From: Daniil Lashin Date: Tue, 4 Dec 2018 12:50:47 +0300 Subject: [PATCH 29/49] Set minimal reserve and min/max coin supply in CreateCoin tx #171 --- CHANGELOG.md | 1 + core/code/code.go | 1 + core/transaction/create_coin.go | 18 ++++++++++++++++++ 3 files changed, 20 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 63aa01c0c..7b87e2c72 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ BREAKING CHANGES - [api] Switch to RPC protocol - [core] Fix issue with incorrect coin conversion - [core] Limit coins supply to 1,000,000,000,000,000 +- [core] Set minimal reserve and min/max coin supply in CreateCoin tx IMPROVEMENT diff --git a/core/code/code.go b/core/code/code.go index 3021e7f01..189de604f 100644 --- a/core/code/code.go +++ b/core/code/code.go @@ -19,6 +19,7 @@ const ( WrongCrr uint32 = 202 InvalidCoinSymbol uint32 = 203 InvalidCoinName uint32 = 204 + WrongCoinSupply uint32 = 205 // convert CrossConvert uint32 = 301 diff --git a/core/transaction/create_coin.go b/core/transaction/create_coin.go index 15fa70873..0804e6b05 100644 --- a/core/transaction/create_coin.go +++ b/core/transaction/create_coin.go @@ -8,6 +8,7 @@ import ( "github.com/MinterTeam/minter-go-node/core/state" "github.com/MinterTeam/minter-go-node/core/types" "github.com/MinterTeam/minter-go-node/formula" + "github.com/MinterTeam/minter-go-node/helpers" "github.com/tendermint/tendermint/libs/common" "math/big" "regexp" @@ -16,6 +17,11 @@ import ( const maxCoinNameBytes = 64 const allowedCoinSymbols = "^[A-Z0-9]{3,10}$" +var ( + minCoinSupply = helpers.BipToPip(big.NewInt(1)) + minCoinReserve = helpers.BipToPip(big.NewInt(1)) +) + type CreateCoinData struct { Name string `json:"name"` Symbol types.CoinSymbol `json:"symbol"` @@ -59,6 +65,18 @@ func (data CreateCoinData) BasicCheck(tx *Transaction, context *state.StateDB) * Log: fmt.Sprintf("Constant Reserve Ratio should be between 10 and 100")} } + if data.InitialAmount.Cmp(MaxCoinSupply) != -1 || data.InitialAmount.Cmp(minCoinSupply) != 1 { + return &Response{ + Code: code.WrongCoinSupply, + Log: fmt.Sprintf("Coin supply should be between %s and %s", minCoinSupply.String(), MaxCoinSupply.String())} + } + + if data.InitialReserve.Cmp(minCoinReserve) != 1 { + return &Response{ + Code: code.WrongCoinSupply, + Log: fmt.Sprintf("Coin reserve should be greater than %s", minCoinReserve.String())} + } + return nil } From ef6c91d8b064362b1df2e638a5f228c4bf564def Mon Sep 17 00:00:00 2001 From: Daniil Lashin Date: Tue, 4 Dec 2018 12:58:39 +0300 Subject: [PATCH 30/49] Fix --- core/code/code.go | 2 +- core/transaction/buy_coin.go | 12 +++++++----- core/transaction/create_coin.go | 2 +- core/transaction/sell_coin.go | 4 ++-- core/transaction/transaction.go | 2 +- 5 files changed, 12 insertions(+), 10 deletions(-) diff --git a/core/code/code.go b/core/code/code.go index 189de604f..331e47b3b 100644 --- a/core/code/code.go +++ b/core/code/code.go @@ -12,7 +12,7 @@ const ( TxPayloadTooLarge uint32 = 109 TxServiceDataTooLarge uint32 = 110 InvalidMultisendData uint32 = 111 - CoinSupplyOverflow uint32 = 112 + CoinSupplyOverflow uint32 = 112 // coin creation CoinAlreadyExists uint32 = 201 diff --git a/core/transaction/buy_coin.go b/core/transaction/buy_coin.go index 8c191dcdf..ff6d3f218 100644 --- a/core/transaction/buy_coin.go +++ b/core/transaction/buy_coin.go @@ -34,12 +34,14 @@ func (data BuyCoinData) TotalSpend(tx *Transaction, context *state.StateDB) (Tot commissionInBaseCoin := tx.CommissionInBaseCoin() commissionIncluded := false - coin := context.GetStateCoin(data.CoinToBuy) + if !data.CoinToBuy.IsBaseCoin() { + coin := context.GetStateCoin(data.CoinToBuy).Data() - if err := CheckForCoinSupplyOverflow(coin.Volume(), data.ValueToBuy); err != nil { - return nil, nil, nil, &Response{ - Code: code.CoinSupplyOverflow, - Log: err.Error(), + if err := CheckForCoinSupplyOverflow(coin.Volume, data.ValueToBuy); err != nil { + return nil, nil, nil, &Response{ + Code: code.CoinSupplyOverflow, + Log: err.Error(), + } } } diff --git a/core/transaction/create_coin.go b/core/transaction/create_coin.go index 0804e6b05..965ba2852 100644 --- a/core/transaction/create_coin.go +++ b/core/transaction/create_coin.go @@ -18,7 +18,7 @@ const maxCoinNameBytes = 64 const allowedCoinSymbols = "^[A-Z0-9]{3,10}$" var ( - minCoinSupply = helpers.BipToPip(big.NewInt(1)) + minCoinSupply = helpers.BipToPip(big.NewInt(1)) minCoinReserve = helpers.BipToPip(big.NewInt(1)) ) diff --git a/core/transaction/sell_coin.go b/core/transaction/sell_coin.go index 7d21b5c47..0e70d0983 100644 --- a/core/transaction/sell_coin.go +++ b/core/transaction/sell_coin.go @@ -39,7 +39,7 @@ func (data SellCoinData) TotalSpend(tx *Transaction, context *state.StateDB) (To if err := CheckForCoinSupplyOverflow(coin.Volume, value); err != nil { return nil, nil, nil, &Response{ Code: code.CoinSupplyOverflow, - Log: err.Error(), + Log: err.Error(), } } @@ -115,7 +115,7 @@ func (data SellCoinData) TotalSpend(tx *Transaction, context *state.StateDB) (To if err := CheckForCoinSupplyOverflow(coinTo.Volume, value); err != nil { return nil, nil, nil, &Response{ Code: code.CoinSupplyOverflow, - Log: err.Error(), + Log: err.Error(), } } diff --git a/core/transaction/transaction.go b/core/transaction/transaction.go index 5efdae3b1..a87e189e7 100644 --- a/core/transaction/transaction.go +++ b/core/transaction/transaction.go @@ -35,7 +35,7 @@ const ( var ( ErrInvalidSig = errors.New("invalid transaction v, r, s values") - MaxCoinSupply = big.NewInt(0).Exp(big.NewInt(1000), big.NewInt(5 + 18), nil) // 1,000,000,000,000,000 bips + MaxCoinSupply = big.NewInt(0).Exp(big.NewInt(1000), big.NewInt(5+18), nil) // 1,000,000,000,000,000 bips ) type Transaction struct { From c92cdb8d70a52daa61f15486b2eb91df9972943c Mon Sep 17 00:00:00 2001 From: Daniil Lashin Date: Tue, 4 Dec 2018 13:39:42 +0300 Subject: [PATCH 31/49] Fix commission issues in BuyCoin tx --- core/transaction/buy_coin.go | 46 ++++++++++++++++++++++++++++++++---- 1 file changed, 41 insertions(+), 5 deletions(-) diff --git a/core/transaction/buy_coin.go b/core/transaction/buy_coin.go index ff6d3f218..a1f37bc61 100644 --- a/core/transaction/buy_coin.go +++ b/core/transaction/buy_coin.go @@ -49,13 +49,28 @@ func (data BuyCoinData) TotalSpend(tx *Transaction, context *state.StateDB) (Tot if data.CoinToSell.IsBaseCoin() { coin := context.GetStateCoin(data.CoinToBuy).Data() + value = formula.CalculatePurchaseAmount(coin.Volume, coin.ReserveBalance, coin.Crr, data.ValueToBuy) if tx.GasCoin == data.CoinToBuy { - // commissionIncluded = true - // TODO: DO SMTH + commissionIncluded = true + + nVolume := big.NewInt(0).Set(coin.Volume) + nVolume.Add(nVolume, data.ValueToBuy) + + nReserveBalance := big.NewInt(0).Set(coin.ReserveBalance) + nReserveBalance.Add(nReserveBalance, value) + + commission := formula.CalculateSaleAmount(nVolume, nReserveBalance, coin.Crr, commissionInBaseCoin) + + total.Add(tx.GasCoin, commission) + conversions = append(conversions, Conversion{ + FromCoin: tx.GasCoin, + FromAmount: commission, + FromReserve: commissionInBaseCoin, + ToCoin: types.GetBaseCoin(), + }) } - value = formula.CalculatePurchaseAmount(coin.Volume, coin.ReserveBalance, coin.Crr, data.ValueToBuy) total.Add(data.CoinToSell, value) conversions = append(conversions, Conversion{ FromCoin: data.CoinToSell, @@ -103,8 +118,23 @@ func (data BuyCoinData) TotalSpend(tx *Transaction, context *state.StateDB) (Tot } if tx.GasCoin == data.CoinToBuy { - // commissionIncluded = true - // TODO: DO SMTH + commissionIncluded = true + + nVolume := big.NewInt(0).Set(coinTo.Volume) + nVolume.Add(nVolume, valueToBuy) + + nReserveBalance := big.NewInt(0).Set(coinTo.ReserveBalance) + nReserveBalance.Add(nReserveBalance, baseCoinNeeded) + + commission := formula.CalculateSaleAmount(nVolume, nReserveBalance, coinTo.Crr, commissionInBaseCoin) + + total.Add(tx.GasCoin, commission) + conversions = append(conversions, Conversion{ + FromCoin: tx.GasCoin, + FromAmount: commission, + FromReserve: commissionInBaseCoin, + ToCoin: types.GetBaseCoin(), + }) } value = formula.CalculateSaleAmount(coinFrom.Volume, coinFrom.ReserveBalance, coinFrom.Crr, baseCoinNeeded) @@ -136,6 +166,12 @@ func (data BuyCoinData) TotalSpend(tx *Transaction, context *state.StateDB) (Tot } commission = formula.CalculateSaleAmount(coin.Volume(), coin.ReserveBalance(), coin.Data().Crr, commissionInBaseCoin) + conversions = append(conversions, Conversion{ + FromCoin: tx.GasCoin, + FromAmount: commission, + FromReserve: commissionInBaseCoin, + ToCoin: types.GetBaseCoin(), + }) } total.Add(tx.GasCoin, commission) From 9eafd14004a161fe1f54346815dd2b1974010f30 Mon Sep 17 00:00:00 2001 From: Daniil Lashin Date: Tue, 4 Dec 2018 13:44:16 +0300 Subject: [PATCH 32/49] Fix commission issues in SellCoin tx --- core/transaction/sell_coin.go | 53 +++++++++++++++++++++++++++-------- 1 file changed, 42 insertions(+), 11 deletions(-) diff --git a/core/transaction/sell_coin.go b/core/transaction/sell_coin.go index 0e70d0983..1116eee14 100644 --- a/core/transaction/sell_coin.go +++ b/core/transaction/sell_coin.go @@ -32,8 +32,23 @@ func (data SellCoinData) TotalSpend(tx *Transaction, context *state.StateDB) (To value = formula.CalculatePurchaseReturn(coin.Volume, coin.ReserveBalance, coin.Crr, data.ValueToSell) if tx.GasCoin == data.CoinToBuy { - // commissionIncluded = true - // TODO: DO SMTH + commissionIncluded = true + + nVolume := big.NewInt(0).Set(coin.Volume) + nVolume.Add(nVolume, value) + + nReserveBalance := big.NewInt(0).Set(coin.ReserveBalance) + nReserveBalance.Add(nReserveBalance, data.ValueToSell) + + commission := formula.CalculateSaleAmount(nVolume, nReserveBalance, coin.Crr, commissionInBaseCoin) + + total.Add(tx.GasCoin, commission) + conversions = append(conversions, Conversion{ + FromCoin: tx.GasCoin, + FromAmount: commission, + FromReserve: commissionInBaseCoin, + ToCoin: types.GetBaseCoin(), + }) } if err := CheckForCoinSupplyOverflow(coin.Volume, value); err != nil { @@ -71,11 +86,6 @@ func (data SellCoinData) TotalSpend(tx *Transaction, context *state.StateDB) (To rValue.Add(rValue, commissionInBaseCoin) } - if tx.GasCoin == data.CoinToBuy { - // commissionIncluded = true - // TODO: DO SMTH - } - total.Add(data.CoinToSell, valueToSell) conversions = append(conversions, Conversion{ FromCoin: data.CoinToSell, @@ -105,12 +115,27 @@ func (data SellCoinData) TotalSpend(tx *Transaction, context *state.StateDB) (To basecoinValue.Add(basecoinValue, commissionInBaseCoin) } + value = formula.CalculatePurchaseReturn(coinTo.Volume, coinTo.ReserveBalance, coinTo.Crr, basecoinValue) + if tx.GasCoin == data.CoinToBuy { - // commissionIncluded = true - // TODO: DO SMTH - } + commissionIncluded = true - value = formula.CalculatePurchaseReturn(coinTo.Volume, coinTo.ReserveBalance, coinTo.Crr, basecoinValue) + nVolume := big.NewInt(0).Set(coinTo.Volume) + nVolume.Add(nVolume, value) + + nReserveBalance := big.NewInt(0).Set(coinTo.ReserveBalance) + nReserveBalance.Add(nReserveBalance, basecoinValue) + + commission := formula.CalculateSaleAmount(nVolume, nReserveBalance, coinTo.Crr, commissionInBaseCoin) + + total.Add(tx.GasCoin, commission) + conversions = append(conversions, Conversion{ + FromCoin: tx.GasCoin, + FromAmount: commission, + FromReserve: commissionInBaseCoin, + ToCoin: types.GetBaseCoin(), + }) + } if err := CheckForCoinSupplyOverflow(coinTo.Volume, value); err != nil { return nil, nil, nil, &Response{ @@ -148,6 +173,12 @@ func (data SellCoinData) TotalSpend(tx *Transaction, context *state.StateDB) (To } commission = formula.CalculateSaleAmount(coin.Volume(), coin.ReserveBalance(), coin.Data().Crr, commissionInBaseCoin) + conversions = append(conversions, Conversion{ + FromCoin: tx.GasCoin, + FromAmount: commission, + FromReserve: commissionInBaseCoin, + ToCoin: types.GetBaseCoin(), + }) } total.Add(tx.GasCoin, commission) From e2527e8982ed58fd4e69de57869d730fafaca442 Mon Sep 17 00:00:00 2001 From: Daniil Lashin Date: Tue, 4 Dec 2018 15:45:45 +0300 Subject: [PATCH 33/49] Separate events from block in API --- CHANGELOG.md | 1 + api/api.go | 1 + api/block.go | 9 --------- api/events.go | 21 +++++++++++++++++++++ docs/api.rst | 8 ++++++++ 5 files changed, 31 insertions(+), 9 deletions(-) create mode 100644 api/events.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b87e2c72..d8aa47d61 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ BREAKING CHANGES - [api] Switch to RPC protocol +- [api] Separate events from block in API - [core] Fix issue with incorrect coin conversion - [core] Limit coins supply to 1,000,000,000,000,000 - [core] Set minimal reserve and min/max coin supply in CreateCoin tx diff --git a/api/api.go b/api/api.go index 5ee603b68..939490f3c 100644 --- a/api/api.go +++ b/api/api.go @@ -43,6 +43,7 @@ var Routes = map[string]*rpcserver.RPCFunc{ "transaction": rpcserver.NewRPCFunc(Transaction, "hash"), "transactions": rpcserver.NewRPCFunc(Transactions, "query"), "block": rpcserver.NewRPCFunc(Block, "height"), + "events": rpcserver.NewRPCFunc(Events, "height"), "net_info": rpcserver.NewRPCFunc(NetInfo, ""), "coin_info": rpcserver.NewRPCFunc(CoinInfo, "symbol,height"), "estimate_coin_sell": rpcserver.NewRPCFunc(EstimateCoinSell, "coin_to_sell,coin_to_buy,value_to_sell,height"), diff --git a/api/block.go b/api/block.go index 08f051185..91d6b7115 100644 --- a/api/block.go +++ b/api/block.go @@ -6,19 +6,12 @@ import ( "github.com/MinterTeam/minter-go-node/core/rewards" "github.com/MinterTeam/minter-go-node/core/transaction" "github.com/MinterTeam/minter-go-node/core/types" - "github.com/MinterTeam/minter-go-node/eventsdb" "github.com/tendermint/tendermint/libs/common" tmtypes "github.com/tendermint/tendermint/types" "math/big" "time" ) -var edb *eventsdb.EventsDB - -func init() { - edb = eventsdb.NewEventsDB(eventsdb.GetCurrentDB()) -} - type BlockResponse struct { Hash common.HexBytes `json:"hash"` Height int64 `json:"height"` @@ -26,7 +19,6 @@ type BlockResponse struct { NumTxs int64 `json:"num_txs"` TotalTxs int64 `json:"total_txs"` Transactions []BlockTransactionResponse `json:"transactions"` - Events eventsdb.Events `json:"events,omitempty"` Precommits []*tmtypes.Vote `json:"precommits"` BlockReward *big.Int `json:"block_reward"` Size int `json:"size"` @@ -107,6 +99,5 @@ func Block(height int64) (*BlockResponse, error) { Precommits: block.Block.LastCommit.Precommits, BlockReward: rewards.GetRewardForBlock(uint64(height)), Size: len(cdc.MustMarshalBinaryLengthPrefixed(block)), - Events: edb.LoadEvents(height), }, nil } diff --git a/api/events.go b/api/events.go new file mode 100644 index 000000000..3419e4788 --- /dev/null +++ b/api/events.go @@ -0,0 +1,21 @@ +package api + +import ( + "github.com/MinterTeam/minter-go-node/eventsdb" +) + +var edb *eventsdb.EventsDB + +func init() { + edb = eventsdb.NewEventsDB(eventsdb.GetCurrentDB()) +} + +type EventsResponse struct { + Events eventsdb.Events `json:"events"` +} + +func Events(height int64) (*EventsResponse, error) { + return &EventsResponse{ + Events: edb.LoadEvents(height), + }, nil +} diff --git a/docs/api.rst b/docs/api.rst index e2b35b785..44fb47e51 100755 --- a/docs/api.rst +++ b/docs/api.rst @@ -306,6 +306,14 @@ Returns block data at given height. } } +Events +^^^^^^ + +Returns events at given height. + +.. code-block:: bash + + curl -s 'localhost:8841/events?height={height}' Coin Info From 0d7d1d91bce2109d6ace8f37d826d4a8a5c1dc05 Mon Sep 17 00:00:00 2001 From: Daniil Lashin Date: Tue, 4 Dec 2018 15:59:31 +0300 Subject: [PATCH 34/49] Update GUI --- gui/a_gui-packr.go | 2 +- gui/html/index.html | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/gui/a_gui-packr.go b/gui/a_gui-packr.go index 274058c26..0b2b53357 100644 --- a/gui/a_gui-packr.go +++ b/gui/a_gui-packr.go @@ -7,5 +7,5 @@ import "github.com/gobuffalo/packr" // You can use the "packr clean" command to clean up this, // and any other packr generated files. func init() { - packr.PackJSONBytes("./html", "index.html", "\"PGh0bWw+CjxoZWFkPgogICAgPG1ldGEgY2hhcnNldD0iVVRGLTgiPgogICAgPG1ldGEgbmFtZT0idmlld3BvcnQiCiAgICAgICAgICBjb250ZW50PSJ3aWR0aD1kZXZpY2Utd2lkdGgsIHVzZXItc2NhbGFibGU9bm8sIGluaXRpYWwtc2NhbGU9MS4wLCBtYXhpbXVtLXNjYWxlPTEuMCwgbWluaW11bS1zY2FsZT0xLjAiPgogICAgPG1ldGEgaHR0cC1lcXVpdj0iWC1VQS1Db21wYXRpYmxlIiBjb250ZW50PSJpZT1lZGdlIj4KICAgIDx0aXRsZT5NaW50ZXIgTm9kZSBHVUk8L3RpdGxlPgogICAgPGxpbmsgcmVsPSJzdHlsZXNoZWV0IiBocmVmPSJodHRwczovL3N0YWNrcGF0aC5ib290c3RyYXBjZG4uY29tL2Jvb3RzdHJhcC80LjEuMC9jc3MvYm9vdHN0cmFwLm1pbi5jc3MiCiAgICAgICAgICBpbnRlZ3JpdHk9InNoYTM4NC05Z1ZRNGRZRnd3V1NqSURabkxFV254Q2plU1dGcGhKaXdHUFhyMWpkZEloT2VnaXUxRndPNXFSR3ZGWE9kSlo0IiBjcm9zc29yaWdpbj0iYW5vbnltb3VzIj4KICAgIDxsaW5rIHJlbD0ic3R5bGVzaGVldCIgaHJlZj0iaHR0cHM6Ly91c2UuZm9udGF3ZXNvbWUuY29tL3JlbGVhc2VzL3Y1LjEuMS9jc3MvYWxsLmNzcyIgaW50ZWdyaXR5PSJzaGEzODQtTzh3aFMzZmhHMk9uQTVLYXMwWTlsM2NmcG1ZamFwakkwRTR0aGVINGl1TUQrcExoYmY2SkkwaklNZlljSzN5WiIgY3Jvc3NvcmlnaW49ImFub255bW91cyI+CiAgICA8c2NyaXB0IHNyYz0iaHR0cHM6Ly9jZG4uanNkZWxpdnIubmV0L25wbS92dWUiPjwvc2NyaXB0PgogICAgPHNjcmlwdCBzcmM9Imh0dHBzOi8vY2RuanMuY2xvdWRmbGFyZS5jb20vYWpheC9saWJzL2F4aW9zLzAuMTguMC9heGlvcy5taW4uanMiPjwvc2NyaXB0PgogICAgPHNjcmlwdCBzcmM9Imh0dHBzOi8vY2RuanMuY2xvdWRmbGFyZS5jb20vYWpheC9saWJzL2NyeXB0by1qcy8zLjEuOS0xL2NyeXB0by1qcy5taW4uanMiPjwvc2NyaXB0PgogICAgPHN0eWxlPgoKICAgICAgICAuY2FyZCB7CiAgICAgICAgICAgIG1hcmdpbi1ib3R0b206IDIwcHg7CiAgICAgICAgfQoKICAgICAgICBodG1sLGJvZHksLmJvZHksI2FwcCB7CiAgICAgICAgICAgIG1pbi1oZWlnaHQ6IDEwMCU7CiAgICAgICAgfQogICAgICAgIAogICAgICAgIC5ib2R5IHsKICAgICAgICAgICAgcGFkZGluZy10b3A6IDE1cHg7CiAgICAgICAgfQoKICAgICAgICAudGFibGUgewogICAgICAgICAgICBtYXJnaW4tYm90dG9tOiAwOwogICAgICAgICAgICB0YWJsZS1sYXlvdXQ6IGZpeGVkOwogICAgICAgIH0KCiAgICAgICAgLmNhcmQtaGVhZGVyIHsKICAgICAgICAgICAgZm9udC13ZWlnaHQ6IGJvbGQ7CiAgICAgICAgfQoKICAgICAgICAuY2FyZC1oZWFkZXIgewogICAgICAgICAgICBwYWRkaW5nLWxlZnQ6IDEycHg7CiAgICAgICAgfQoKICAgICAgICAuaCB7CiAgICAgICAgICAgIHdpZHRoOiAyMDBweDsKICAgICAgICAgICAgYmFja2dyb3VuZC1jb2xvcjogI2YzZjNmMzsKICAgICAgICAgICAgYm9yZGVyLXJpZ2h0OiAxcHggc29saWQgI2NjYzsKICAgICAgICB9CgogICAgICAgIC5iZy1zdWNjZXNzLCAuYmctZGFuZ2VyIHsKICAgICAgICAgICAgY29sb3I6IHdoaXRlOwogICAgICAgIH0KCiAgICAgICAgLmJnLWRhbmdlciB7CiAgICAgICAgICAgIGJvcmRlci1jb2xvcjogI2RjMzU0NSAhaW1wb3J0YW50OwogICAgICAgIH0KCiAgICAgICAgLmJnLXN1Y2Nlc3MgewogICAgICAgICAgICBib3JkZXItY29sb3I6ICMyOGE3NDUgIWltcG9ydGFudDsKICAgICAgICB9CgogICAgICAgIC5mYS1jaGVjayB7CiAgICAgICAgICAgIGNvbG9yOiBncmVlbjsKICAgICAgICB9CgogICAgICAgIC5mYS1leGNsYW1hdGlvbi1jaXJjbGUgewogICAgICAgICAgICBjb2xvcjogcmVkOwogICAgICAgIH0KICAgIDwvc3R5bGU+CjwvaGVhZD4KPGJvZHkgc3R5bGU9ImJhY2tncm91bmQtY29sb3I6ICMzNDNhNDAxYSI+CjxkaXYgaWQ9ImFwcCI+CiAgICA8bmF2IGNsYXNzPSJuYXZiYXIgbmF2YmFyLWV4cGFuZC1sZyBuYXZiYXItZGFyayBiZy1kYXJrIj4KICAgICAgICA8ZGl2IGNsYXNzPSJjb250YWluZXIiPgogICAgICAgICAgICA8c3BhbiBjbGFzcz0ibmF2YmFyLWJyYW5kIG1iLTAgaDEiPjxpIGNsYXNzPSJmYXMgZmEtdGVybWluYWwiPjwvaT4gJm5ic3A7IE1pbnRlciBGdWxsIE5vZGUgU3RhdHVzPC9zcGFuPgogICAgICAgIDwvZGl2PgogICAgPC9uYXY+CiAgICA8ZGl2IGNsYXNzPSJjb250YWluZXIgYm9keSIgdi1pZj0iZXJyb3IiPgogICAgICAgIDxkaXYgY2xhc3M9ImFsZXJ0IGFsZXJ0LWRhbmdlciIgcm9sZT0iYWxlcnQiPgogICAgICAgICAgICA8aDQgY2xhc3M9ImFsZXJ0LWhlYWRpbmciPkVycm9yIHdoaWxlIGNvbm5lY3RpbmcgdG8gbG9jYWwgbm9kZTwvaDQ+CiAgICAgICAgICAgIDxwIGNsYXNzPSJtYi0wIj57eyBlcnJvciB9fTwvcD4KICAgICAgICA8L2Rpdj4KICAgIDwvZGl2PgogICAgPGRpdiBjbGFzcz0iY29udGFpbmVyIGJvZHkgYmctd2hpdGUiIHYtaWY9InN0YXR1cyAmJiAhZXJyb3IiPgogICAgICAgIDxkaXYgY2xhc3M9InJvdyI+CiAgICAgICAgICAgIDxkaXYgY2xhc3M9ImNvbCI+CiAgICAgICAgICAgICAgICA8ZGl2IGNsYXNzPSJjYXJkIj4KICAgICAgICAgICAgICAgICAgICA8ZGl2IGNsYXNzPSJjYXJkLWhlYWRlciI+CiAgICAgICAgICAgICAgICAgICAgICAgIE5vZGUgSW5mbwogICAgICAgICAgICAgICAgICAgIDwvZGl2PgogICAgICAgICAgICAgICAgICAgIDx0YWJsZSBjbGFzcz0idGFibGUgY2FyZC1ib2R5Ij4KICAgICAgICAgICAgICAgICAgICAgICAgPHRib2R5PgogICAgICAgICAgICAgICAgICAgICAgICA8dHI+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dGQgY2xhc3M9ImgiPk1vbmlrZXI8L3RkPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgPHRkPnt7IHN0YXR1cy5ub2RlX2luZm8ubW9uaWtlciB9fTwvdGQ+CiAgICAgICAgICAgICAgICAgICAgICAgIDwvdHI+CiAgICAgICAgICAgICAgICAgICAgICAgIDx0cj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0ZCBjbGFzcz0iaCI+Tm9kZSBJRDwvdGQ+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dGQ+e3sgc3RhdHVzLm5vZGVfaW5mby5pZCB9fTwvdGQ+CiAgICAgICAgICAgICAgICAgICAgICAgIDwvdHI+CiAgICAgICAgICAgICAgICAgICAgICAgIDx0cj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0ZCBjbGFzcz0iaCI+TmV0d29yayBJRDwvdGQ+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dGQ+e3sgc3RhdHVzLm5vZGVfaW5mby5uZXR3b3JrIH19PC90ZD4KICAgICAgICAgICAgICAgICAgICAgICAgPC90cj4KICAgICAgICAgICAgICAgICAgICAgICAgPHRyPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgPHRkIGNsYXNzPSJoIj5NaW50ZXIgVmVyc2lvbjwvdGQ+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dGQ+e3sgdmVyc2lvbiB9fTwvdGQ+CiAgICAgICAgICAgICAgICAgICAgICAgIDwvdHI+CiAgICAgICAgICAgICAgICAgICAgICAgIDx0cj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0ZCBjbGFzcz0iaCI+VGVuZGVybWludCBWZXJzaW9uPC90ZD4KICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0ZD57eyBzdGF0dXMubm9kZV9pbmZvLnZlcnNpb24gfX08L3RkPgogICAgICAgICAgICAgICAgICAgICAgICA8L3RyPgogICAgICAgICAgICAgICAgICAgICAgICA8L3Rib2R5PgogICAgICAgICAgICAgICAgICAgIDwvdGFibGU+CiAgICAgICAgICAgICAgICA8L2Rpdj4KICAgICAgICAgICAgICAgIDxkaXYgY2xhc3M9ImNhcmQiIHYtaWY9Im5ldF9pbmZvIj4KICAgICAgICAgICAgICAgICAgICA8ZGl2IGNsYXNzPSJjYXJkLWhlYWRlciI+CiAgICAgICAgICAgICAgICAgICAgICAgIE5ldCBJbmZvCiAgICAgICAgICAgICAgICAgICAgPC9kaXY+CiAgICAgICAgICAgICAgICAgICAgPHRhYmxlIGNsYXNzPSJ0YWJsZSBjYXJkLWJvZHkiPgogICAgICAgICAgICAgICAgICAgICAgICA8dGJvZHk+CiAgICAgICAgICAgICAgICAgICAgICAgIDx0cj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0ZCBjbGFzcz0iaCI+SXMgTGlzdGVuaW5nPC90ZD4KICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0ZD48aSA6Y2xhc3M9InsnZmEtY2hlY2snOiBuZXRfaW5mby5saXN0ZW5pbmd9IiBjbGFzcz0iZmFzIj48L2k+PC90ZD4KICAgICAgICAgICAgICAgICAgICAgICAgPC90cj4KICAgICAgICAgICAgICAgICAgICAgICAgPHRyPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgPHRkIGNsYXNzPSJoIj5Db25uZWN0ZWQgUGVlcnM8L3RkPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgPHRkPnt7IG5ldF9pbmZvLm5fcGVlcnMgfX0gPGkgOmNsYXNzPSJ7J2ZhLWV4Y2xhbWF0aW9uLWNpcmNsZSc6IG5ldF9pbmZvLm5fcGVlcnMgPCAxfSIgY2xhc3M9ImZhcyI+PC9pPjwvdGQ+CiAgICAgICAgICAgICAgICAgICAgICAgIDwvdHI+CiAgICAgICAgICAgICAgICAgICAgICAgIDwvdGJvZHk+CiAgICAgICAgICAgICAgICAgICAgPC90YWJsZT4KICAgICAgICAgICAgICAgIDwvZGl2PgogICAgICAgICAgICA8L2Rpdj4KICAgICAgICAgICAgPGRpdiBjbGFzcz0iY29sIj4KICAgICAgICAgICAgICAgIDxkaXYgY2xhc3M9ImNhcmQiPgogICAgICAgICAgICAgICAgICAgIDxkaXYgY2xhc3M9ImNhcmQtaGVhZGVyIj4KICAgICAgICAgICAgICAgICAgICAgICAgU3luY2luZyBJbmZvCiAgICAgICAgICAgICAgICAgICAgPC9kaXY+CiAgICAgICAgICAgICAgICAgICAgPHRhYmxlIGNsYXNzPSJ0YWJsZSBjYXJkLWJvZHkiPgogICAgICAgICAgICAgICAgICAgICAgICA8dGJvZHk+CiAgICAgICAgICAgICAgICAgICAgICAgIDx0cj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0ZCBjbGFzcz0iaCI+SXMgU3luY2VkPC90ZD4KICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0ZD4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8c3BhbiB2LWlmPSJzdGF0dXMuc3luY19pbmZvLmNhdGNoaW5nX3VwIj5Obzwvc3Bhbj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8c3BhbiB2LWlmPSIhc3RhdHVzLnN5bmNfaW5mby5jYXRjaGluZ191cCI+WWVzPC9zcGFuPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDxpIDpjbGFzcz0ieydmYS1jaGVjayc6ICFzdGF0dXMuc3luY19pbmZvLmNhdGNoaW5nX3VwLCAnZmEtZXhjbGFtYXRpb24tY2lyY2xlJzogc3RhdHVzLnN5bmNfaW5mby5jYXRjaGluZ191cH0iIGNsYXNzPSJmYXMiPjwvaT48L3RkPgogICAgICAgICAgICAgICAgICAgICAgICA8L3RyPgogICAgICAgICAgICAgICAgICAgICAgICA8dHI+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dGQgY2xhc3M9ImgiPkxhdGVzdCBCbG9jayBIZWlnaHQ8L3RkPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgPHRkPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICN7eyBzdGF0dXMuc3luY19pbmZvLmxhdGVzdF9ibG9ja19oZWlnaHQgfX0gPHNwYW4gdi1pZj0ibWFzdGVyU3RhdHVzICYmIE51bWJlcihzdGF0dXMuc3luY19pbmZvLmxhdGVzdF9ibG9ja19oZWlnaHQpIDw9IE51bWJlcihtYXN0ZXJTdGF0dXMubGF0ZXN0X2Jsb2NrX2hlaWdodCkiIGNsYXNzPSJ0ZXh0LW11dGVkIj5vZiB7eyBtYXN0ZXJTdGF0dXMubGF0ZXN0X2Jsb2NrX2hlaWdodCB9fTwvc3Bhbj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvdGQ+CiAgICAgICAgICAgICAgICAgICAgICAgIDwvdHI+CiAgICAgICAgICAgICAgICAgICAgICAgIDx0cj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0ZCBjbGFzcz0iaCI+TGF0ZXN0IEJsb2NrIFRpbWU8L3RkPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgPHRkPgogICAgICAgICAgICAgICAgICAgICAgICAgICAge3sgc3RhdHVzLnN5bmNfaW5mby5sYXRlc3RfYmxvY2tfdGltZSB9fQogICAgICAgICAgICAgICAgICAgICAgICAgICAgPC90ZD4KICAgICAgICAgICAgICAgICAgICAgICAgPC90cj4KICAgICAgICAgICAgICAgICAgICAgICAgPC90Ym9keT4KICAgICAgICAgICAgICAgICAgICA8L3RhYmxlPgogICAgICAgICAgICAgICAgPC9kaXY+CiAgICAgICAgICAgICAgICA8ZGl2IGNsYXNzPSJjYXJkIj4KICAgICAgICAgICAgICAgICAgICA8ZGl2IGNsYXNzPSJjYXJkLWhlYWRlciI+CiAgICAgICAgICAgICAgICAgICAgICAgIFZhbGlkYXRvciBJbmZvCiAgICAgICAgICAgICAgICAgICAgPC9kaXY+CiAgICAgICAgICAgICAgICAgICAgPHRhYmxlIGNsYXNzPSJ0YWJsZSBjYXJkLWJvZHkiPgogICAgICAgICAgICAgICAgICAgICAgICA8dGJvZHk+CiAgICAgICAgICAgICAgICAgICAgICAgIDx0cj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0ZD5QdWJsaWMgS2V5PC90ZD4KICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0ZD57eyB2YWxpZGF0b3JQdWJLZXkgfX08L3RkPgogICAgICAgICAgICAgICAgICAgICAgICA8L3RyPgogICAgICAgICAgICAgICAgICAgICAgICA8dHI+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dGQ+U3RhdHVzPC90ZD4KICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0ZD57eyB2YWxpZGF0b3JTdGF0dXMgfX08L3RkPgogICAgICAgICAgICAgICAgICAgICAgICA8L3RyPgogICAgICAgICAgICAgICAgICAgICAgICA8dHI+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dGQ+VG90YWwgU3Rha2U8L3RkPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgPHRkPnt7IHN0YWtlIH19IE1OVDwvdGQ+CiAgICAgICAgICAgICAgICAgICAgICAgIDwvdHI+CiAgICAgICAgICAgICAgICAgICAgICAgIDx0cj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0ZD5Wb3RpbmcgUG93ZXI8L3RkPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgPHRkPnt7IG5pY2VOdW0oc3RhdHVzLnZhbGlkYXRvcl9pbmZvLnZvdGluZ19wb3dlcikgfX0gPHNwYW4gY2xhc3M9InRleHQtbXV0ZWQiPm9mIDEwMCwwMDAsMDAwPC9zcGFuPjwvdGQ+CiAgICAgICAgICAgICAgICAgICAgICAgIDwvdHI+CiAgICAgICAgICAgICAgICAgICAgICAgIDwvdGJvZHk+CiAgICAgICAgICAgICAgICAgICAgPC90YWJsZT4KICAgICAgICAgICAgICAgIDwvZGl2PgogICAgICAgICAgICA8L2Rpdj4KICAgICAgICA8L2Rpdj4KICAgIDwvZGl2Pgo8L2Rpdj4KPHNjcmlwdD4KICAgIG5ldyBWdWUoewogICAgICAgIGVsOiAnI2FwcCcsCiAgICAgICAgZGF0YTogewogICAgICAgICAgICBtYXN0ZXJTdGF0dXM6IG51bGwsCiAgICAgICAgICAgIHN0YXR1czogbnVsbCwKICAgICAgICAgICAgdmVyc2lvbjogbnVsbCwKICAgICAgICAgICAgbmV0X2luZm86IG51bGwsCiAgICAgICAgICAgIGVycm9yOiBudWxsLAogICAgICAgICAgICB2YWxpZGF0b3JQdWJLZXk6ICcuLi4nLAogICAgICAgICAgICB2YWxpZGF0b3JTdGF0dXM6ICcuLi4nLAogICAgICAgICAgICBzdGFrZTogJy4uLicKICAgICAgICB9LAogICAgICAgIG1vdW50ZWQoKSB7CiAgICAgICAgICAgIHRoaXMucmVmcmVzaCgpCiAgICAgICAgfSwKICAgICAgICBtZXRob2RzOiB7CiAgICAgICAgICAgIG5pY2VOdW0obnVtKSB7CiAgICAgICAgICAgICAgICByZXR1cm4gbnVtLnRvU3RyaW5nKCkucmVwbGFjZSgvXEIoPz0oXGR7M30pKyg/IVxkKSkvZywgIiwiKQogICAgICAgICAgICB9LAogICAgICAgICAgICBiYXNlNjRUb0hleChiYXNlNjQpIHsKICAgICAgICAgICAgICAgIHJldHVybiBDcnlwdG9KUy5lbmMuQmFzZTY0LnBhcnNlKGJhc2U2NCkudG9TdHJpbmcoKQogICAgICAgICAgICB9LAogICAgICAgICAgICByZWZyZXNoKCkgewogICAgICAgICAgICAgICAgYXhpb3MuYWxsKFsKICAgICAgICAgICAgICAgICAgICBheGlvcy5nZXQoIi8vIiArIHdpbmRvdy5sb2NhdGlvbi5ob3N0bmFtZSArICc6ODg0MS9hcGkvc3RhdHVzJyksCiAgICAgICAgICAgICAgICAgICAgYXhpb3MuZ2V0KCIvLyIgKyB3aW5kb3cubG9jYXRpb24uaG9zdG5hbWUgKyAnOjg4NDEvYXBpL25ldF9pbmZvJyksCiAgICAgICAgICAgICAgICBdKS50aGVuKGF4aW9zLnNwcmVhZChmdW5jdGlvbiAoc3RhdHVzLCBuZXRfaW5mbykgewogICAgICAgICAgICAgICAgICAgIHRoaXMuZXJyb3IgPSBudWxsCgogICAgICAgICAgICAgICAgICAgIHRoaXMuc3RhdHVzID0gc3RhdHVzLmRhdGEucmVzdWx0LnRtX3N0YXR1cwogICAgICAgICAgICAgICAgICAgIHRoaXMudmVyc2lvbiA9IHN0YXR1cy5kYXRhLnJlc3VsdC52ZXJzaW9uCiAgICAgICAgICAgICAgICAgICAgdGhpcy5uZXRfaW5mbyA9IG5ldF9pbmZvLmRhdGEucmVzdWx0CgogICAgICAgICAgICAgICAgICAgIHRoaXMudmFsaWRhdG9yUHViS2V5ID0gJ01wJyArIHRoaXMuYmFzZTY0VG9IZXgoc3RhdHVzLmRhdGEucmVzdWx0LnRtX3N0YXR1cy52YWxpZGF0b3JfaW5mby5wdWJfa2V5LnZhbHVlKQoKICAgICAgICAgICAgICAgICAgICBheGlvcy5hbGwoWwogICAgICAgICAgICAgICAgICAgICAgICBheGlvcy5nZXQoIi8vIiArIHdpbmRvdy5sb2NhdGlvbi5ob3N0bmFtZSArICc6ODg0MS9hcGkvdmFsaWRhdG9ycycpLAogICAgICAgICAgICAgICAgICAgICAgICBheGlvcy5nZXQoIi8vIiArIHdpbmRvdy5sb2NhdGlvbi5ob3N0bmFtZSArICc6ODg0MS9hcGkvY2FuZGlkYXRlLycgKyB0aGlzLnZhbGlkYXRvclB1YktleSksCiAgICAgICAgICAgICAgICAgICAgXSkudGhlbihheGlvcy5zcHJlYWQoZnVuY3Rpb24gKHZhbGlkYXRvcnMsIGNhbmRpZGF0ZSkgewoKICAgICAgICAgICAgICAgICAgICAgICAgdGhpcy5zdGFrZSA9IE1hdGgucm91bmQoY2FuZGlkYXRlLmRhdGEucmVzdWx0LmNhbmRpZGF0ZS50b3RhbF9zdGFrZSAvIE1hdGgucG93KDEwLCAxNykpIC8gMTAKCiAgICAgICAgICAgICAgICAgICAgICAgIGlmICh2YWxpZGF0b3JzLmRhdGEucmVzdWx0LmZpbmQoZnVuY3Rpb24odmFsKSB7IHJldHVybiB2YWwuY2FuZGlkYXRlLnB1Yl9rZXkgPT09IHRoaXMudmFsaWRhdG9yUHViS2V5IH0uYmluZCh0aGlzKSkpIHsKICAgICAgICAgICAgICAgICAgICAgICAgICAgIHRoaXMudmFsaWRhdG9yU3RhdHVzID0gJ1ZhbGlkYXRpbmcnOwogICAgICAgICAgICAgICAgICAgICAgICAgICAgcmV0dXJuCiAgICAgICAgICAgICAgICAgICAgICAgIH0KCiAgICAgICAgICAgICAgICAgICAgICAgIGlmIChjYW5kaWRhdGUuZGF0YS5yZXN1bHQuY2FuZGlkYXRlLnN0YXR1cyA9PT0gMikgewogICAgICAgICAgICAgICAgICAgICAgICAgICAgdGhpcy52YWxpZGF0b3JTdGF0dXMgPSAnQ2FuZGlkYXRlJwogICAgICAgICAgICAgICAgICAgICAgICAgICAgcmV0dXJuCiAgICAgICAgICAgICAgICAgICAgICAgIH0KCiAgICAgICAgICAgICAgICAgICAgICAgIHRoaXMudmFsaWRhdG9yU3RhdHVzID0gJ0Rvd24nOwogICAgICAgICAgICAgICAgICAgIH0uYmluZCh0aGlzKSkpLmNhdGNoKGZ1bmN0aW9uKCkgIHsKICAgICAgICAgICAgICAgICAgICAgICAgdGhpcy52YWxpZGF0b3JTdGF0dXMgPSAnTm90IGRlY2xhcmVkJzsKICAgICAgICAgICAgICAgICAgICAgICAgdGhpcy5zdGFrZSA9IDA7CiAgICAgICAgICAgICAgICAgICAgfS5iaW5kKHRoaXMpKTsKCiAgICAgICAgICAgICAgICAgICAgc2V0VGltZW91dCh0aGlzLnJlZnJlc2gsIDUwMDApCiAgICAgICAgICAgICAgICB9LmJpbmQodGhpcykpKS5jYXRjaChmdW5jdGlvbiAocmVhc29uKSB7CiAgICAgICAgICAgICAgICAgICAgdGhpcy5lcnJvciA9IHJlYXNvbi50b1N0cmluZygpOwogICAgICAgICAgICAgICAgICAgIHNldFRpbWVvdXQodGhpcy5yZWZyZXNoLCA1MDAwKQogICAgICAgICAgICAgICAgfS5iaW5kKHRoaXMpKQoKICAgICAgICAgICAgICAgIGF4aW9zLmdldCgiaHR0cHM6Ly9taW50ZXItbm9kZS0xLnRlc3RuZXQubWludGVyLm5ldHdvcmsvYXBpL3N0YXR1cyIpLnRoZW4oZnVuY3Rpb24gKG1hc3RlclN0YXR1cykgewogICAgICAgICAgICAgICAgICAgIHRoaXMubWFzdGVyU3RhdHVzID0gbWFzdGVyU3RhdHVzLmRhdGEucmVzdWx0CiAgICAgICAgICAgICAgICB9LmJpbmQodGhpcykpCiAgICAgICAgICAgIH0KICAgICAgICB9CiAgICB9KQo8L3NjcmlwdD4KPC9ib2R5Pgo8L2h0bWw+\"") + packr.PackJSONBytes("./html", "index.html", "\"PGh0bWw+CjxoZWFkPgogICAgPG1ldGEgY2hhcnNldD0iVVRGLTgiPgogICAgPG1ldGEgbmFtZT0idmlld3BvcnQiCiAgICAgICAgICBjb250ZW50PSJ3aWR0aD1kZXZpY2Utd2lkdGgsIHVzZXItc2NhbGFibGU9bm8sIGluaXRpYWwtc2NhbGU9MS4wLCBtYXhpbXVtLXNjYWxlPTEuMCwgbWluaW11bS1zY2FsZT0xLjAiPgogICAgPG1ldGEgaHR0cC1lcXVpdj0iWC1VQS1Db21wYXRpYmxlIiBjb250ZW50PSJpZT1lZGdlIj4KICAgIDx0aXRsZT5NaW50ZXIgTm9kZSBHVUk8L3RpdGxlPgogICAgPGxpbmsgcmVsPSJzdHlsZXNoZWV0IiBocmVmPSJodHRwczovL3N0YWNrcGF0aC5ib290c3RyYXBjZG4uY29tL2Jvb3RzdHJhcC80LjEuMC9jc3MvYm9vdHN0cmFwLm1pbi5jc3MiCiAgICAgICAgICBpbnRlZ3JpdHk9InNoYTM4NC05Z1ZRNGRZRnd3V1NqSURabkxFV254Q2plU1dGcGhKaXdHUFhyMWpkZEloT2VnaXUxRndPNXFSR3ZGWE9kSlo0IiBjcm9zc29yaWdpbj0iYW5vbnltb3VzIj4KICAgIDxsaW5rIHJlbD0ic3R5bGVzaGVldCIgaHJlZj0iaHR0cHM6Ly91c2UuZm9udGF3ZXNvbWUuY29tL3JlbGVhc2VzL3Y1LjEuMS9jc3MvYWxsLmNzcyIgaW50ZWdyaXR5PSJzaGEzODQtTzh3aFMzZmhHMk9uQTVLYXMwWTlsM2NmcG1ZamFwakkwRTR0aGVINGl1TUQrcExoYmY2SkkwaklNZlljSzN5WiIgY3Jvc3NvcmlnaW49ImFub255bW91cyI+CiAgICA8c2NyaXB0IHNyYz0iaHR0cHM6Ly9jZG4uanNkZWxpdnIubmV0L25wbS92dWUiPjwvc2NyaXB0PgogICAgPHNjcmlwdCBzcmM9Imh0dHBzOi8vY2RuanMuY2xvdWRmbGFyZS5jb20vYWpheC9saWJzL2F4aW9zLzAuMTguMC9heGlvcy5taW4uanMiPjwvc2NyaXB0PgogICAgPHNjcmlwdCBzcmM9Imh0dHBzOi8vY2RuanMuY2xvdWRmbGFyZS5jb20vYWpheC9saWJzL2NyeXB0by1qcy8zLjEuOS0xL2NyeXB0by1qcy5taW4uanMiPjwvc2NyaXB0PgogICAgPHN0eWxlPgoKICAgICAgICAuY2FyZCB7CiAgICAgICAgICAgIG1hcmdpbi1ib3R0b206IDIwcHg7CiAgICAgICAgfQoKICAgICAgICBodG1sLGJvZHksLmJvZHksI2FwcCB7CiAgICAgICAgICAgIG1pbi1oZWlnaHQ6IDEwMCU7CiAgICAgICAgfQogICAgICAgIAogICAgICAgIC5ib2R5IHsKICAgICAgICAgICAgcGFkZGluZy10b3A6IDE1cHg7CiAgICAgICAgfQoKICAgICAgICAudGFibGUgewogICAgICAgICAgICBtYXJnaW4tYm90dG9tOiAwOwogICAgICAgICAgICB0YWJsZS1sYXlvdXQ6IGZpeGVkOwogICAgICAgIH0KCiAgICAgICAgLmNhcmQtaGVhZGVyIHsKICAgICAgICAgICAgZm9udC13ZWlnaHQ6IGJvbGQ7CiAgICAgICAgfQoKICAgICAgICAuY2FyZC1oZWFkZXIgewogICAgICAgICAgICBwYWRkaW5nLWxlZnQ6IDEycHg7CiAgICAgICAgfQoKICAgICAgICAuaCB7CiAgICAgICAgICAgIHdpZHRoOiAyMDBweDsKICAgICAgICAgICAgYmFja2dyb3VuZC1jb2xvcjogI2YzZjNmMzsKICAgICAgICAgICAgYm9yZGVyLXJpZ2h0OiAxcHggc29saWQgI2NjYzsKICAgICAgICB9CgogICAgICAgIC5iZy1zdWNjZXNzLCAuYmctZGFuZ2VyIHsKICAgICAgICAgICAgY29sb3I6IHdoaXRlOwogICAgICAgIH0KCiAgICAgICAgLmJnLWRhbmdlciB7CiAgICAgICAgICAgIGJvcmRlci1jb2xvcjogI2RjMzU0NSAhaW1wb3J0YW50OwogICAgICAgIH0KCiAgICAgICAgLmJnLXN1Y2Nlc3MgewogICAgICAgICAgICBib3JkZXItY29sb3I6ICMyOGE3NDUgIWltcG9ydGFudDsKICAgICAgICB9CgogICAgICAgIC5mYS1jaGVjayB7CiAgICAgICAgICAgIGNvbG9yOiBncmVlbjsKICAgICAgICB9CgogICAgICAgIC5mYS1leGNsYW1hdGlvbi1jaXJjbGUgewogICAgICAgICAgICBjb2xvcjogcmVkOwogICAgICAgIH0KICAgIDwvc3R5bGU+CjwvaGVhZD4KPGJvZHkgc3R5bGU9ImJhY2tncm91bmQtY29sb3I6ICMzNDNhNDAxYSI+CjxkaXYgaWQ9ImFwcCI+CiAgICA8bmF2IGNsYXNzPSJuYXZiYXIgbmF2YmFyLWV4cGFuZC1sZyBuYXZiYXItZGFyayBiZy1kYXJrIj4KICAgICAgICA8ZGl2IGNsYXNzPSJjb250YWluZXIiPgogICAgICAgICAgICA8c3BhbiBjbGFzcz0ibmF2YmFyLWJyYW5kIG1iLTAgaDEiPjxpIGNsYXNzPSJmYXMgZmEtdGVybWluYWwiPjwvaT4gJm5ic3A7IE1pbnRlciBGdWxsIE5vZGUgU3RhdHVzPC9zcGFuPgogICAgICAgIDwvZGl2PgogICAgPC9uYXY+CiAgICA8ZGl2IGNsYXNzPSJjb250YWluZXIgYm9keSIgdi1pZj0iZXJyb3IiPgogICAgICAgIDxkaXYgY2xhc3M9ImFsZXJ0IGFsZXJ0LWRhbmdlciIgcm9sZT0iYWxlcnQiPgogICAgICAgICAgICA8aDQgY2xhc3M9ImFsZXJ0LWhlYWRpbmciPkVycm9yIHdoaWxlIGNvbm5lY3RpbmcgdG8gbG9jYWwgbm9kZTwvaDQ+CiAgICAgICAgICAgIDxwIGNsYXNzPSJtYi0wIj57eyBlcnJvciB9fTwvcD4KICAgICAgICA8L2Rpdj4KICAgIDwvZGl2PgogICAgPGRpdiBjbGFzcz0iY29udGFpbmVyIGJvZHkgYmctd2hpdGUiIHYtaWY9InN0YXR1cyAmJiAhZXJyb3IiPgogICAgICAgIDxkaXYgY2xhc3M9InJvdyI+CiAgICAgICAgICAgIDxkaXYgY2xhc3M9ImNvbCI+CiAgICAgICAgICAgICAgICA8ZGl2IGNsYXNzPSJjYXJkIj4KICAgICAgICAgICAgICAgICAgICA8ZGl2IGNsYXNzPSJjYXJkLWhlYWRlciI+CiAgICAgICAgICAgICAgICAgICAgICAgIE5vZGUgSW5mbwogICAgICAgICAgICAgICAgICAgIDwvZGl2PgogICAgICAgICAgICAgICAgICAgIDx0YWJsZSBjbGFzcz0idGFibGUgY2FyZC1ib2R5Ij4KICAgICAgICAgICAgICAgICAgICAgICAgPHRib2R5PgogICAgICAgICAgICAgICAgICAgICAgICA8dHI+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dGQgY2xhc3M9ImgiPk1vbmlrZXI8L3RkPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgPHRkPnt7IHN0YXR1cy5ub2RlX2luZm8ubW9uaWtlciB9fTwvdGQ+CiAgICAgICAgICAgICAgICAgICAgICAgIDwvdHI+CiAgICAgICAgICAgICAgICAgICAgICAgIDx0cj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0ZCBjbGFzcz0iaCI+Tm9kZSBJRDwvdGQ+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dGQ+e3sgc3RhdHVzLm5vZGVfaW5mby5pZCB9fTwvdGQ+CiAgICAgICAgICAgICAgICAgICAgICAgIDwvdHI+CiAgICAgICAgICAgICAgICAgICAgICAgIDx0cj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0ZCBjbGFzcz0iaCI+TmV0d29yayBJRDwvdGQ+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dGQ+e3sgc3RhdHVzLm5vZGVfaW5mby5uZXR3b3JrIH19PC90ZD4KICAgICAgICAgICAgICAgICAgICAgICAgPC90cj4KICAgICAgICAgICAgICAgICAgICAgICAgPHRyPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgPHRkIGNsYXNzPSJoIj5NaW50ZXIgVmVyc2lvbjwvdGQ+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dGQ+e3sgdmVyc2lvbiB9fTwvdGQ+CiAgICAgICAgICAgICAgICAgICAgICAgIDwvdHI+CiAgICAgICAgICAgICAgICAgICAgICAgIDx0cj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0ZCBjbGFzcz0iaCI+VGVuZGVybWludCBWZXJzaW9uPC90ZD4KICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0ZD57eyBzdGF0dXMubm9kZV9pbmZvLnZlcnNpb24gfX08L3RkPgogICAgICAgICAgICAgICAgICAgICAgICA8L3RyPgogICAgICAgICAgICAgICAgICAgICAgICA8L3Rib2R5PgogICAgICAgICAgICAgICAgICAgIDwvdGFibGU+CiAgICAgICAgICAgICAgICA8L2Rpdj4KICAgICAgICAgICAgICAgIDxkaXYgY2xhc3M9ImNhcmQiIHYtaWY9Im5ldF9pbmZvIj4KICAgICAgICAgICAgICAgICAgICA8ZGl2IGNsYXNzPSJjYXJkLWhlYWRlciI+CiAgICAgICAgICAgICAgICAgICAgICAgIE5ldCBJbmZvCiAgICAgICAgICAgICAgICAgICAgPC9kaXY+CiAgICAgICAgICAgICAgICAgICAgPHRhYmxlIGNsYXNzPSJ0YWJsZSBjYXJkLWJvZHkiPgogICAgICAgICAgICAgICAgICAgICAgICA8dGJvZHk+CiAgICAgICAgICAgICAgICAgICAgICAgIDx0cj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0ZCBjbGFzcz0iaCI+SXMgTGlzdGVuaW5nPC90ZD4KICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0ZD48aSA6Y2xhc3M9InsnZmEtY2hlY2snOiBuZXRfaW5mby5saXN0ZW5pbmd9IiBjbGFzcz0iZmFzIj48L2k+PC90ZD4KICAgICAgICAgICAgICAgICAgICAgICAgPC90cj4KICAgICAgICAgICAgICAgICAgICAgICAgPHRyPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgPHRkIGNsYXNzPSJoIj5Db25uZWN0ZWQgUGVlcnM8L3RkPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgPHRkPnt7IG5ldF9pbmZvLm5fcGVlcnMgfX0gPGkgOmNsYXNzPSJ7J2ZhLWV4Y2xhbWF0aW9uLWNpcmNsZSc6IG5ldF9pbmZvLm5fcGVlcnMgPCAxfSIgY2xhc3M9ImZhcyI+PC9pPjwvdGQ+CiAgICAgICAgICAgICAgICAgICAgICAgIDwvdHI+CiAgICAgICAgICAgICAgICAgICAgICAgIDwvdGJvZHk+CiAgICAgICAgICAgICAgICAgICAgPC90YWJsZT4KICAgICAgICAgICAgICAgIDwvZGl2PgogICAgICAgICAgICA8L2Rpdj4KICAgICAgICAgICAgPGRpdiBjbGFzcz0iY29sIj4KICAgICAgICAgICAgICAgIDxkaXYgY2xhc3M9ImNhcmQiPgogICAgICAgICAgICAgICAgICAgIDxkaXYgY2xhc3M9ImNhcmQtaGVhZGVyIj4KICAgICAgICAgICAgICAgICAgICAgICAgU3luY2luZyBJbmZvCiAgICAgICAgICAgICAgICAgICAgPC9kaXY+CiAgICAgICAgICAgICAgICAgICAgPHRhYmxlIGNsYXNzPSJ0YWJsZSBjYXJkLWJvZHkiPgogICAgICAgICAgICAgICAgICAgICAgICA8dGJvZHk+CiAgICAgICAgICAgICAgICAgICAgICAgIDx0cj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0ZCBjbGFzcz0iaCI+SXMgU3luY2VkPC90ZD4KICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0ZD4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8c3BhbiB2LWlmPSJzdGF0dXMuc3luY19pbmZvLmNhdGNoaW5nX3VwIj5Obzwvc3Bhbj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8c3BhbiB2LWlmPSIhc3RhdHVzLnN5bmNfaW5mby5jYXRjaGluZ191cCI+WWVzPC9zcGFuPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDxpIDpjbGFzcz0ieydmYS1jaGVjayc6ICFzdGF0dXMuc3luY19pbmZvLmNhdGNoaW5nX3VwLCAnZmEtZXhjbGFtYXRpb24tY2lyY2xlJzogc3RhdHVzLnN5bmNfaW5mby5jYXRjaGluZ191cH0iIGNsYXNzPSJmYXMiPjwvaT48L3RkPgogICAgICAgICAgICAgICAgICAgICAgICA8L3RyPgogICAgICAgICAgICAgICAgICAgICAgICA8dHI+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dGQgY2xhc3M9ImgiPkxhdGVzdCBCbG9jayBIZWlnaHQ8L3RkPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgPHRkPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICN7eyBzdGF0dXMuc3luY19pbmZvLmxhdGVzdF9ibG9ja19oZWlnaHQgfX0gPHNwYW4gdi1pZj0ibWFzdGVyU3RhdHVzICYmIE51bWJlcihzdGF0dXMuc3luY19pbmZvLmxhdGVzdF9ibG9ja19oZWlnaHQpIDw9IE51bWJlcihtYXN0ZXJTdGF0dXMubGF0ZXN0X2Jsb2NrX2hlaWdodCkiIGNsYXNzPSJ0ZXh0LW11dGVkIj5vZiB7eyBtYXN0ZXJTdGF0dXMubGF0ZXN0X2Jsb2NrX2hlaWdodCB9fTwvc3Bhbj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvdGQ+CiAgICAgICAgICAgICAgICAgICAgICAgIDwvdHI+CiAgICAgICAgICAgICAgICAgICAgICAgIDx0cj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0ZCBjbGFzcz0iaCI+TGF0ZXN0IEJsb2NrIFRpbWU8L3RkPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgPHRkPgogICAgICAgICAgICAgICAgICAgICAgICAgICAge3sgc3RhdHVzLnN5bmNfaW5mby5sYXRlc3RfYmxvY2tfdGltZSB9fQogICAgICAgICAgICAgICAgICAgICAgICAgICAgPC90ZD4KICAgICAgICAgICAgICAgICAgICAgICAgPC90cj4KICAgICAgICAgICAgICAgICAgICAgICAgPC90Ym9keT4KICAgICAgICAgICAgICAgICAgICA8L3RhYmxlPgogICAgICAgICAgICAgICAgPC9kaXY+CiAgICAgICAgICAgICAgICA8ZGl2IGNsYXNzPSJjYXJkIj4KICAgICAgICAgICAgICAgICAgICA8ZGl2IGNsYXNzPSJjYXJkLWhlYWRlciI+CiAgICAgICAgICAgICAgICAgICAgICAgIFZhbGlkYXRvciBJbmZvCiAgICAgICAgICAgICAgICAgICAgPC9kaXY+CiAgICAgICAgICAgICAgICAgICAgPHRhYmxlIGNsYXNzPSJ0YWJsZSBjYXJkLWJvZHkiPgogICAgICAgICAgICAgICAgICAgICAgICA8dGJvZHk+CiAgICAgICAgICAgICAgICAgICAgICAgIDx0cj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0ZD5QdWJsaWMgS2V5PC90ZD4KICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0ZD57eyB2YWxpZGF0b3JQdWJLZXkgfX08L3RkPgogICAgICAgICAgICAgICAgICAgICAgICA8L3RyPgogICAgICAgICAgICAgICAgICAgICAgICA8dHI+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dGQ+U3RhdHVzPC90ZD4KICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0ZD57eyB2YWxpZGF0b3JTdGF0dXMgfX08L3RkPgogICAgICAgICAgICAgICAgICAgICAgICA8L3RyPgogICAgICAgICAgICAgICAgICAgICAgICA8dHI+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dGQ+VG90YWwgU3Rha2U8L3RkPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgPHRkPnt7IHN0YWtlIH19IE1OVDwvdGQ+CiAgICAgICAgICAgICAgICAgICAgICAgIDwvdHI+CiAgICAgICAgICAgICAgICAgICAgICAgIDx0cj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0ZD5Wb3RpbmcgUG93ZXI8L3RkPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgPHRkPnt7IG5pY2VOdW0oc3RhdHVzLnZhbGlkYXRvcl9pbmZvLnZvdGluZ19wb3dlcikgfX0gPHNwYW4gY2xhc3M9InRleHQtbXV0ZWQiPm9mIDEwMCwwMDAsMDAwPC9zcGFuPjwvdGQ+CiAgICAgICAgICAgICAgICAgICAgICAgIDwvdHI+CiAgICAgICAgICAgICAgICAgICAgICAgIDwvdGJvZHk+CiAgICAgICAgICAgICAgICAgICAgPC90YWJsZT4KICAgICAgICAgICAgICAgIDwvZGl2PgogICAgICAgICAgICA8L2Rpdj4KICAgICAgICA8L2Rpdj4KICAgIDwvZGl2Pgo8L2Rpdj4KPHNjcmlwdD4KICAgIG5ldyBWdWUoewogICAgICAgIGVsOiAnI2FwcCcsCiAgICAgICAgZGF0YTogewogICAgICAgICAgICBtYXN0ZXJTdGF0dXM6IG51bGwsCiAgICAgICAgICAgIHN0YXR1czogbnVsbCwKICAgICAgICAgICAgdmVyc2lvbjogbnVsbCwKICAgICAgICAgICAgbmV0X2luZm86IG51bGwsCiAgICAgICAgICAgIGVycm9yOiBudWxsLAogICAgICAgICAgICB2YWxpZGF0b3JQdWJLZXk6ICcuLi4nLAogICAgICAgICAgICB2YWxpZGF0b3JTdGF0dXM6ICcuLi4nLAogICAgICAgICAgICBzdGFrZTogJy4uLicKICAgICAgICB9LAogICAgICAgIG1vdW50ZWQoKSB7CiAgICAgICAgICAgIHRoaXMucmVmcmVzaCgpCiAgICAgICAgfSwKICAgICAgICBtZXRob2RzOiB7CiAgICAgICAgICAgIG5pY2VOdW0obnVtKSB7CiAgICAgICAgICAgICAgICByZXR1cm4gbnVtLnRvU3RyaW5nKCkucmVwbGFjZSgvXEIoPz0oXGR7M30pKyg/IVxkKSkvZywgIiwiKQogICAgICAgICAgICB9LAogICAgICAgICAgICBiYXNlNjRUb0hleChiYXNlNjQpIHsKICAgICAgICAgICAgICAgIHJldHVybiBDcnlwdG9KUy5lbmMuQmFzZTY0LnBhcnNlKGJhc2U2NCkudG9TdHJpbmcoKQogICAgICAgICAgICB9LAogICAgICAgICAgICByZWZyZXNoKCkgewogICAgICAgICAgICAgICAgYXhpb3MuYWxsKFsKICAgICAgICAgICAgICAgICAgICBheGlvcy5nZXQoIi8vIiArIHdpbmRvdy5sb2NhdGlvbi5ob3N0bmFtZSArICc6ODg0MS9zdGF0dXMnKSwKICAgICAgICAgICAgICAgICAgICBheGlvcy5nZXQoIi8vIiArIHdpbmRvdy5sb2NhdGlvbi5ob3N0bmFtZSArICc6ODg0MS9uZXRfaW5mbycpLAogICAgICAgICAgICAgICAgXSkudGhlbihheGlvcy5zcHJlYWQoZnVuY3Rpb24gKHN0YXR1cywgbmV0X2luZm8pIHsKICAgICAgICAgICAgICAgICAgICB0aGlzLmVycm9yID0gbnVsbAoKICAgICAgICAgICAgICAgICAgICB0aGlzLnN0YXR1cyA9IHN0YXR1cy5kYXRhLnJlc3VsdC50bV9zdGF0dXMKICAgICAgICAgICAgICAgICAgICB0aGlzLnZlcnNpb24gPSBzdGF0dXMuZGF0YS5yZXN1bHQudmVyc2lvbgogICAgICAgICAgICAgICAgICAgIHRoaXMubmV0X2luZm8gPSBuZXRfaW5mby5kYXRhLnJlc3VsdAoKICAgICAgICAgICAgICAgICAgICB0aGlzLnZhbGlkYXRvclB1YktleSA9ICdNcCcgKyB0aGlzLmJhc2U2NFRvSGV4KHN0YXR1cy5kYXRhLnJlc3VsdC50bV9zdGF0dXMudmFsaWRhdG9yX2luZm8ucHViX2tleS52YWx1ZSkKCiAgICAgICAgICAgICAgICAgICAgYXhpb3MuYWxsKFsKICAgICAgICAgICAgICAgICAgICAgICAgYXhpb3MuZ2V0KCIvLyIgKyB3aW5kb3cubG9jYXRpb24uaG9zdG5hbWUgKyAnOjg4NDEvdmFsaWRhdG9ycycpLAogICAgICAgICAgICAgICAgICAgICAgICBheGlvcy5nZXQoIi8vIiArIHdpbmRvdy5sb2NhdGlvbi5ob3N0bmFtZSArICc6ODg0MS9jYW5kaWRhdGU/cHVia2V5PScgKyB0aGlzLnZhbGlkYXRvclB1YktleSksCiAgICAgICAgICAgICAgICAgICAgXSkudGhlbihheGlvcy5zcHJlYWQoZnVuY3Rpb24gKHZhbGlkYXRvcnMsIGNhbmRpZGF0ZSkgewoKICAgICAgICAgICAgICAgICAgICAgICAgdGhpcy5zdGFrZSA9IE1hdGgucm91bmQoY2FuZGlkYXRlLmRhdGEucmVzdWx0LnRvdGFsX3N0YWtlIC8gTWF0aC5wb3coMTAsIDE3KSkgLyAxMAoKICAgICAgICAgICAgICAgICAgICAgICAgaWYgKHZhbGlkYXRvcnMuZGF0YS5yZXN1bHQuZmluZChmdW5jdGlvbih2YWwpIHsgcmV0dXJuIHZhbC5jYW5kaWRhdGUucHViX2tleSA9PT0gdGhpcy52YWxpZGF0b3JQdWJLZXkgfS5iaW5kKHRoaXMpKSkgewogICAgICAgICAgICAgICAgICAgICAgICAgICAgdGhpcy52YWxpZGF0b3JTdGF0dXMgPSAnVmFsaWRhdGluZyc7CiAgICAgICAgICAgICAgICAgICAgICAgICAgICByZXR1cm4KICAgICAgICAgICAgICAgICAgICAgICAgfQoKICAgICAgICAgICAgICAgICAgICAgICAgaWYgKGNhbmRpZGF0ZS5kYXRhLnJlc3VsdC5zdGF0dXMgPT09IDIpIHsKICAgICAgICAgICAgICAgICAgICAgICAgICAgIHRoaXMudmFsaWRhdG9yU3RhdHVzID0gJ0NhbmRpZGF0ZScKICAgICAgICAgICAgICAgICAgICAgICAgICAgIHJldHVybgogICAgICAgICAgICAgICAgICAgICAgICB9CgogICAgICAgICAgICAgICAgICAgICAgICB0aGlzLnZhbGlkYXRvclN0YXR1cyA9ICdEb3duJzsKICAgICAgICAgICAgICAgICAgICB9LmJpbmQodGhpcykpKS5jYXRjaChmdW5jdGlvbigpICB7CiAgICAgICAgICAgICAgICAgICAgICAgIHRoaXMudmFsaWRhdG9yU3RhdHVzID0gJ05vdCBkZWNsYXJlZCc7CiAgICAgICAgICAgICAgICAgICAgICAgIHRoaXMuc3Rha2UgPSAwOwogICAgICAgICAgICAgICAgICAgIH0uYmluZCh0aGlzKSk7CgogICAgICAgICAgICAgICAgICAgIHNldFRpbWVvdXQodGhpcy5yZWZyZXNoLCA1MDAwKQogICAgICAgICAgICAgICAgfS5iaW5kKHRoaXMpKSkuY2F0Y2goZnVuY3Rpb24gKHJlYXNvbikgewogICAgICAgICAgICAgICAgICAgIHRoaXMuZXJyb3IgPSByZWFzb24udG9TdHJpbmcoKTsKICAgICAgICAgICAgICAgICAgICBzZXRUaW1lb3V0KHRoaXMucmVmcmVzaCwgNTAwMCkKICAgICAgICAgICAgICAgIH0uYmluZCh0aGlzKSkKCiAgICAgICAgICAgICAgICBheGlvcy5nZXQoImh0dHBzOi8vbWludGVyLW5vZGUtMS50ZXN0bmV0Lm1pbnRlci5uZXR3b3JrL3N0YXR1cyIpLnRoZW4oZnVuY3Rpb24gKG1hc3RlclN0YXR1cykgewogICAgICAgICAgICAgICAgICAgIHRoaXMubWFzdGVyU3RhdHVzID0gbWFzdGVyU3RhdHVzLmRhdGEucmVzdWx0CiAgICAgICAgICAgICAgICB9LmJpbmQodGhpcykpCiAgICAgICAgICAgIH0KICAgICAgICB9CiAgICB9KQo8L3NjcmlwdD4KPC9ib2R5Pgo8L2h0bWw+\"") } diff --git a/gui/html/index.html b/gui/html/index.html index 5e5f785a1..cf683010e 100644 --- a/gui/html/index.html +++ b/gui/html/index.html @@ -211,8 +211,8 @@

Error while connecting to local node

}, refresh() { axios.all([ - axios.get("//" + window.location.hostname + ':8841/api/status'), - axios.get("//" + window.location.hostname + ':8841/api/net_info'), + axios.get("//" + window.location.hostname + ':8841/status'), + axios.get("//" + window.location.hostname + ':8841/net_info'), ]).then(axios.spread(function (status, net_info) { this.error = null @@ -223,18 +223,18 @@

Error while connecting to local node

this.validatorPubKey = 'Mp' + this.base64ToHex(status.data.result.tm_status.validator_info.pub_key.value) axios.all([ - axios.get("//" + window.location.hostname + ':8841/api/validators'), - axios.get("//" + window.location.hostname + ':8841/api/candidate/' + this.validatorPubKey), + axios.get("//" + window.location.hostname + ':8841/validators'), + axios.get("//" + window.location.hostname + ':8841/candidate?pubkey=' + this.validatorPubKey), ]).then(axios.spread(function (validators, candidate) { - this.stake = Math.round(candidate.data.result.candidate.total_stake / Math.pow(10, 17)) / 10 + this.stake = Math.round(candidate.data.result.total_stake / Math.pow(10, 17)) / 10 if (validators.data.result.find(function(val) { return val.candidate.pub_key === this.validatorPubKey }.bind(this))) { this.validatorStatus = 'Validating'; return } - if (candidate.data.result.candidate.status === 2) { + if (candidate.data.result.status === 2) { this.validatorStatus = 'Candidate' return } @@ -251,7 +251,7 @@

Error while connecting to local node

setTimeout(this.refresh, 5000) }.bind(this)) - axios.get("https://minter-node-1.testnet.minter.network/api/status").then(function (masterStatus) { + axios.get("https://minter-node-1.testnet.minter.network/status").then(function (masterStatus) { this.masterStatus = masterStatus.data.result }.bind(this)) } From 3c815a6fce8373a0e9658a67796dc39cb6768d7a Mon Sep 17 00:00:00 2001 From: Daniil Lashin Date: Tue, 4 Dec 2018 16:12:19 +0300 Subject: [PATCH 35/49] Add MinimumValueToBuy and MaximumValueToSell to convert transactions #156 --- CHANGELOG.md | 1 + core/code/code.go | 4 ++- core/transaction/buy_coin.go | 30 ++++++++++++++++++-- core/transaction/sell_all_coin.go | 47 ++++++++++++++++++++++--------- core/transaction/sell_coin.go | 29 +++++++++++++++++-- docs/transactions.rst | 22 +++++++++------ 6 files changed, 104 insertions(+), 29 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d8aa47d61..482850c45 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ BREAKING CHANGES - [core] Fix issue with incorrect coin conversion - [core] Limit coins supply to 1,000,000,000,000,000 - [core] Set minimal reserve and min/max coin supply in CreateCoin tx +- [core] Add MinimumValueToBuy and MaximumValueToSell to convert transactions IMPROVEMENT diff --git a/core/code/code.go b/core/code/code.go index 331e47b3b..7ffae0aed 100644 --- a/core/code/code.go +++ b/core/code/code.go @@ -22,7 +22,9 @@ const ( WrongCoinSupply uint32 = 205 // convert - CrossConvert uint32 = 301 + CrossConvert uint32 = 301 + MaximumValueToSellReached uint32 = 302 + MinimumValueToBuylReached uint32 = 303 // candidate CandidateExists uint32 = 401 diff --git a/core/transaction/buy_coin.go b/core/transaction/buy_coin.go index a1f37bc61..ea3aec7a2 100644 --- a/core/transaction/buy_coin.go +++ b/core/transaction/buy_coin.go @@ -13,9 +13,10 @@ import ( ) type BuyCoinData struct { - CoinToBuy types.CoinSymbol `json:"coin_to_buy"` - ValueToBuy *big.Int `json:"value_to_buy"` - CoinToSell types.CoinSymbol `json:"coin_to_sell"` + CoinToBuy types.CoinSymbol `json:"coin_to_buy"` + ValueToBuy *big.Int `json:"value_to_buy"` + CoinToSell types.CoinSymbol `json:"coin_to_sell"` + MaximumValueToSell *big.Int `json:"maximum_value_to_sell"` } func (data BuyCoinData) String() string { @@ -51,6 +52,13 @@ func (data BuyCoinData) TotalSpend(tx *Transaction, context *state.StateDB) (Tot coin := context.GetStateCoin(data.CoinToBuy).Data() value = formula.CalculatePurchaseAmount(coin.Volume, coin.ReserveBalance, coin.Crr, data.ValueToBuy) + if value.Cmp(data.MaximumValueToSell) == 1 { + return nil, nil, nil, &Response{ + Code: code.MaximumValueToSellReached, + Log: fmt.Sprintf("You wanted to sell maximum %s, but currently you need to spend %s to complete tx", data.MaximumValueToSell.String(), value.String()), + } + } + if tx.GasCoin == data.CoinToBuy { commissionIncluded = true @@ -88,6 +96,14 @@ func (data BuyCoinData) TotalSpend(tx *Transaction, context *state.StateDB) (Tot coin := context.GetStateCoin(data.CoinToSell).Data() value = formula.CalculateSaleAmount(coin.Volume, coin.ReserveBalance, coin.Crr, valueToBuy) + + if value.Cmp(data.MaximumValueToSell) == 1 { + return nil, nil, nil, &Response{ + Code: code.MaximumValueToSellReached, + Log: fmt.Sprintf("You wanted to sell maximum %s, but currently you need to spend %s to complete tx", data.MaximumValueToSell.String(), value.String()), + } + } + total.Add(data.CoinToSell, value) conversions = append(conversions, Conversion{ FromCoin: data.CoinToSell, @@ -138,6 +154,14 @@ func (data BuyCoinData) TotalSpend(tx *Transaction, context *state.StateDB) (Tot } value = formula.CalculateSaleAmount(coinFrom.Volume, coinFrom.ReserveBalance, coinFrom.Crr, baseCoinNeeded) + + if value.Cmp(data.MaximumValueToSell) == 1 { + return nil, nil, nil, &Response{ + Code: code.MaximumValueToSellReached, + Log: fmt.Sprintf("You wanted to sell maximum %s, but currently you need to spend %s to complete tx", data.MaximumValueToSell.String(), value.String()), + } + } + total.Add(data.CoinToSell, value) conversions = append(conversions, Conversion{ FromCoin: data.CoinToSell, diff --git a/core/transaction/sell_all_coin.go b/core/transaction/sell_all_coin.go index 4d6f60bb4..9572afb9e 100644 --- a/core/transaction/sell_all_coin.go +++ b/core/transaction/sell_all_coin.go @@ -13,8 +13,9 @@ import ( ) type SellAllCoinData struct { - CoinToSell types.CoinSymbol `json:"coin_to_sell"` - CoinToBuy types.CoinSymbol `json:"coin_to_buy"` + CoinToSell types.CoinSymbol `json:"coin_to_sell"` + CoinToBuy types.CoinSymbol `json:"coin_to_buy"` + MinimumValueToBuy *big.Int `json:"minimum_value_to_buy"` } func (data SellAllCoinData) TotalSpend(tx *Transaction, context *state.StateDB) (TotalSpends, []Conversion, *big.Int, *Response) { @@ -87,24 +88,19 @@ func (data SellAllCoinData) Run(tx *Transaction, context *state.StateDB, isCheck amountToSell := big.NewInt(0).Set(available) amountToSell.Sub(amountToSell, commission) - // TODO: move under errors - if !isCheck { - rewardPool.Add(rewardPool, commissionInBaseCoin) - - context.SubBalance(sender, data.CoinToSell, available) - - if !data.CoinToSell.IsBaseCoin() { - context.SubCoinVolume(data.CoinToSell, commission) - context.SubCoinReserve(data.CoinToSell, commissionInBaseCoin) - } - } - var value *big.Int if data.CoinToSell.IsBaseCoin() { coin := context.GetStateCoin(data.CoinToBuy).Data() value = formula.CalculatePurchaseReturn(coin.Volume, coin.ReserveBalance, coin.Crr, amountToSell) + if value.Cmp(data.MinimumValueToBuy) == 1 { + return Response{ + Code: code.MinimumValueToBuylReached, + Log: fmt.Sprintf("You wanted to get minimum %s, but currently you will get %s", data.MinimumValueToBuy.String(), value.String()), + } + } + if !isCheck { context.AddCoinVolume(data.CoinToBuy, value) context.AddCoinReserve(data.CoinToBuy, amountToSell) @@ -113,6 +109,13 @@ func (data SellAllCoinData) Run(tx *Transaction, context *state.StateDB, isCheck coin := context.GetStateCoin(data.CoinToSell).Data() value = formula.CalculateSaleReturn(coin.Volume, coin.ReserveBalance, coin.Crr, amountToSell) + if value.Cmp(data.MinimumValueToBuy) == 1 { + return Response{ + Code: code.MinimumValueToBuylReached, + Log: fmt.Sprintf("You wanted to get minimum %s, but currently you will get %s", data.MinimumValueToBuy.String(), value.String()), + } + } + if !isCheck { context.SubCoinVolume(data.CoinToSell, amountToSell) context.SubCoinReserve(data.CoinToSell, value) @@ -124,6 +127,13 @@ func (data SellAllCoinData) Run(tx *Transaction, context *state.StateDB, isCheck basecoinValue := formula.CalculateSaleReturn(coinFrom.Volume, coinFrom.ReserveBalance, coinFrom.Crr, amountToSell) value = formula.CalculatePurchaseReturn(coinTo.Volume, coinTo.ReserveBalance, coinTo.Crr, basecoinValue) + if value.Cmp(data.MinimumValueToBuy) == 1 { + return Response{ + Code: code.MinimumValueToBuylReached, + Log: fmt.Sprintf("You wanted to get minimum %s, but currently you will get %s", data.MinimumValueToBuy.String(), value.String()), + } + } + if !isCheck { context.AddCoinVolume(data.CoinToBuy, value) context.SubCoinVolume(data.CoinToSell, amountToSell) @@ -134,6 +144,15 @@ func (data SellAllCoinData) Run(tx *Transaction, context *state.StateDB, isCheck } if !isCheck { + rewardPool.Add(rewardPool, commissionInBaseCoin) + + context.SubBalance(sender, data.CoinToSell, available) + + if !data.CoinToSell.IsBaseCoin() { + context.SubCoinVolume(data.CoinToSell, commission) + context.SubCoinReserve(data.CoinToSell, commissionInBaseCoin) + } + context.AddBalance(sender, data.CoinToBuy, value) context.SetNonce(sender, tx.Nonce) } diff --git a/core/transaction/sell_coin.go b/core/transaction/sell_coin.go index 1116eee14..1744b011c 100644 --- a/core/transaction/sell_coin.go +++ b/core/transaction/sell_coin.go @@ -13,9 +13,10 @@ import ( ) type SellCoinData struct { - CoinToSell types.CoinSymbol `json:"coin_to_sell"` - ValueToSell *big.Int `json:"value_to_sell"` - CoinToBuy types.CoinSymbol `json:"coin_to_buy"` + CoinToSell types.CoinSymbol `json:"coin_to_sell"` + ValueToSell *big.Int `json:"value_to_sell"` + CoinToBuy types.CoinSymbol `json:"coin_to_buy"` + MinimumValueToBuy *big.Int `json:"minimum_value_to_buy"` } func (data SellCoinData) TotalSpend(tx *Transaction, context *state.StateDB) (TotalSpends, []Conversion, *big.Int, *Response) { @@ -31,6 +32,13 @@ func (data SellCoinData) TotalSpend(tx *Transaction, context *state.StateDB) (To coin := context.GetStateCoin(data.CoinToBuy).Data() value = formula.CalculatePurchaseReturn(coin.Volume, coin.ReserveBalance, coin.Crr, data.ValueToSell) + if value.Cmp(data.MinimumValueToBuy) == 1 { + return nil, nil, nil, &Response{ + Code: code.MinimumValueToBuylReached, + Log: fmt.Sprintf("You wanted to get minimum %s, but currently you will get %s", data.MinimumValueToBuy.String(), value.String()), + } + } + if tx.GasCoin == data.CoinToBuy { commissionIncluded = true @@ -68,6 +76,14 @@ func (data SellCoinData) TotalSpend(tx *Transaction, context *state.StateDB) (To } else if data.CoinToBuy.IsBaseCoin() { coin := context.GetStateCoin(data.CoinToSell).Data() value = formula.CalculateSaleReturn(coin.Volume, coin.ReserveBalance, coin.Crr, data.ValueToSell) + + if value.Cmp(data.MinimumValueToBuy) == 1 { + return nil, nil, nil, &Response{ + Code: code.MinimumValueToBuylReached, + Log: fmt.Sprintf("You wanted to get minimum %s, but currently you will get %s", data.MinimumValueToBuy.String(), value.String()), + } + } + rValue := big.NewInt(0).Set(value) valueToSell := data.ValueToSell @@ -117,6 +133,13 @@ func (data SellCoinData) TotalSpend(tx *Transaction, context *state.StateDB) (To value = formula.CalculatePurchaseReturn(coinTo.Volume, coinTo.ReserveBalance, coinTo.Crr, basecoinValue) + if value.Cmp(data.MinimumValueToBuy) == 1 { + return nil, nil, nil, &Response{ + Code: code.MinimumValueToBuylReached, + Log: fmt.Sprintf("You wanted to get minimum %s, but currently you will get %s", data.MinimumValueToBuy.String(), value.String()), + } + } + if tx.GasCoin == data.CoinToBuy { commissionIncluded = true diff --git a/docs/transactions.rst b/docs/transactions.rst index 4b08f6068..60f41d855 100755 --- a/docs/transactions.rst +++ b/docs/transactions.rst @@ -131,14 +131,16 @@ Transaction for selling one coin (owned by sender) in favour of another coin in .. code-block:: go type SellCoinData struct { - CoinToSell [10]byte - ValueToSell *big.Int - CoinToBuy [10]byte + CoinToSell [10]byte + ValueToSell *big.Int + CoinToBuy [10]byte + MinimumValueToBuy *big.Int } | **CoinToSell** - Symbol of a coin to give. | **ValueToSell** - Amount of **CoinToSell** to give. | **CoinToBuy** - Symbol of a coin to get. +| **MinimumValueToBuy** - Minimum value of coins to get. Sell all coin transaction ^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -152,12 +154,14 @@ Transaction for selling all existing coins of one type (owned by sender) in favo .. code-block:: go type SellAllCoinData struct { - CoinToSell [10]byte - CoinToBuy [10]byte + CoinToSell [10]byte + CoinToBuy [10]byte + MinimumValueToBuy *big.Int } | **CoinToSell** - Symbol of a coin to give. | **CoinToBuy** - Symbol of a coin to get. +| **MinimumValueToBuy** - Minimum value of coins to get. Buy coin transaction ^^^^^^^^^^^^^^^^^^^^ @@ -171,14 +175,16 @@ Transaction for buy a coin paying another coin (owned by sender). .. code-block:: go type BuyCoinData struct { - CoinToBuy [10]byte - ValueToBuy *big.Int - CoinToSell [10]byte + CoinToBuy [10]byte + ValueToBuy *big.Int + CoinToSell [10]byte + MaximumValueToSell *big.Int } | **CoinToBuy** - Symbol of a coin to get. | **ValueToBuy** - Amount of **CoinToBuy** to get. | **CoinToSell** - Symbol of a coin to give. +| **MaximumValueToSell** - Maximum value of coins to sell. Create coin transaction ^^^^^^^^^^^^^^^^^^^^^^^ From 161a5126ec9ada619a81f24eebf5d435b9efb12e Mon Sep 17 00:00:00 2001 From: Daniil Lashin Date: Tue, 4 Dec 2018 16:17:16 +0300 Subject: [PATCH 36/49] Fix & update tests #156 --- core/transaction/buy_coin_test.go | 32 +++++++++++++++----------- core/transaction/sell_all_coin.go | 6 ++--- core/transaction/sell_all_coin_test.go | 6 +++-- core/transaction/sell_coin.go | 6 ++--- core/transaction/sell_coin_test.go | 8 ++++--- 5 files changed, 34 insertions(+), 24 deletions(-) diff --git a/core/transaction/buy_coin_test.go b/core/transaction/buy_coin_test.go index ab37e714f..52989fb6d 100644 --- a/core/transaction/buy_coin_test.go +++ b/core/transaction/buy_coin_test.go @@ -54,10 +54,12 @@ func TestBuyCoinTx(t *testing.T) { cState.AddBalance(addr, coin, helpers.BipToPip(big.NewInt(1000000))) toBuy := helpers.BipToPip(big.NewInt(10)) + maxValToSell, _ := big.NewInt(0).SetString("159374246010000000000", 10) data := BuyCoinData{ - CoinToBuy: getTestCoinSymbol(), - ValueToBuy: toBuy, - CoinToSell: coin, + CoinToBuy: getTestCoinSymbol(), + ValueToBuy: toBuy, + CoinToSell: coin, + MaximumValueToSell: maxValToSell, } encodedData, err := rlp.EncodeToBytes(data) @@ -115,10 +117,12 @@ func TestBuyCoinTxInsufficientFunds(t *testing.T) { cState.AddBalance(addr, coin, helpers.BipToPip(big.NewInt(1))) toBuy := helpers.BipToPip(big.NewInt(10)) + maxValToSell, _ := big.NewInt(0).SetString("159374246010000000000", 10) data := BuyCoinData{ - CoinToBuy: getTestCoinSymbol(), - ValueToBuy: toBuy, - CoinToSell: coin, + CoinToBuy: getTestCoinSymbol(), + ValueToBuy: toBuy, + CoinToSell: coin, + MaximumValueToSell: maxValToSell, } encodedData, err := rlp.EncodeToBytes(data) @@ -338,9 +342,10 @@ func TestBuyCoinTxNotGasCoin(t *testing.T) { cState.AddBalance(addr, getTestCoinSymbol(), helpers.BipToPip(big.NewInt(1000))) data := BuyCoinData{ - CoinToBuy: types.GetBaseCoin(), - ValueToBuy: big.NewInt(1), - CoinToSell: getTestCoinSymbol(), + CoinToBuy: types.GetBaseCoin(), + ValueToBuy: big.NewInt(1), + CoinToSell: getTestCoinSymbol(), + MaximumValueToSell: big.NewInt(10004502852067863), } encodedData, err := rlp.EncodeToBytes(data) @@ -376,12 +381,13 @@ func TestBuyCoinTxNotGasCoin(t *testing.T) { } func TestBuyCoinTxJSON(t *testing.T) { - out := []byte("{\"coin_to_buy\":\"MNT\",\"value_to_buy\":\"1\",\"coin_to_sell\":\"TEST\"}") + out := []byte("{\"coin_to_buy\":\"MNT\",\"value_to_buy\":\"1\",\"coin_to_sell\":\"TEST\",\"maximum_value_to_sell\":\"1\"}") buyCoinData := BuyCoinData{ - CoinToBuy: types.GetBaseCoin(), - ValueToBuy: big.NewInt(1), - CoinToSell: getTestCoinSymbol(), + CoinToBuy: types.GetBaseCoin(), + ValueToBuy: big.NewInt(1), + CoinToSell: getTestCoinSymbol(), + MaximumValueToSell: big.NewInt(1), } result, err := cdc.MarshalJSON(buyCoinData) diff --git a/core/transaction/sell_all_coin.go b/core/transaction/sell_all_coin.go index 9572afb9e..8fc802938 100644 --- a/core/transaction/sell_all_coin.go +++ b/core/transaction/sell_all_coin.go @@ -94,7 +94,7 @@ func (data SellAllCoinData) Run(tx *Transaction, context *state.StateDB, isCheck coin := context.GetStateCoin(data.CoinToBuy).Data() value = formula.CalculatePurchaseReturn(coin.Volume, coin.ReserveBalance, coin.Crr, amountToSell) - if value.Cmp(data.MinimumValueToBuy) == 1 { + if value.Cmp(data.MinimumValueToBuy) == -1 { return Response{ Code: code.MinimumValueToBuylReached, Log: fmt.Sprintf("You wanted to get minimum %s, but currently you will get %s", data.MinimumValueToBuy.String(), value.String()), @@ -109,7 +109,7 @@ func (data SellAllCoinData) Run(tx *Transaction, context *state.StateDB, isCheck coin := context.GetStateCoin(data.CoinToSell).Data() value = formula.CalculateSaleReturn(coin.Volume, coin.ReserveBalance, coin.Crr, amountToSell) - if value.Cmp(data.MinimumValueToBuy) == 1 { + if value.Cmp(data.MinimumValueToBuy) == -1 { return Response{ Code: code.MinimumValueToBuylReached, Log: fmt.Sprintf("You wanted to get minimum %s, but currently you will get %s", data.MinimumValueToBuy.String(), value.String()), @@ -127,7 +127,7 @@ func (data SellAllCoinData) Run(tx *Transaction, context *state.StateDB, isCheck basecoinValue := formula.CalculateSaleReturn(coinFrom.Volume, coinFrom.ReserveBalance, coinFrom.Crr, amountToSell) value = formula.CalculatePurchaseReturn(coinTo.Volume, coinTo.ReserveBalance, coinTo.Crr, basecoinValue) - if value.Cmp(data.MinimumValueToBuy) == 1 { + if value.Cmp(data.MinimumValueToBuy) == -1 { return Response{ Code: code.MinimumValueToBuylReached, Log: fmt.Sprintf("You wanted to get minimum %s, but currently you will get %s", data.MinimumValueToBuy.String(), value.String()), diff --git a/core/transaction/sell_all_coin_test.go b/core/transaction/sell_all_coin_test.go index 8a4158f1d..8f11e5066 100644 --- a/core/transaction/sell_all_coin_test.go +++ b/core/transaction/sell_all_coin_test.go @@ -20,9 +20,11 @@ func TestSellAllCoinTx(t *testing.T) { cState.AddBalance(addr, coin, helpers.BipToPip(big.NewInt(1000000))) + minValToBuy, _ := big.NewInt(0).SetString("151191152412701306252", 10) data := SellAllCoinData{ - CoinToBuy: getTestCoinSymbol(), - CoinToSell: coin, + CoinToSell: coin, + CoinToBuy: getTestCoinSymbol(), + MinimumValueToBuy: minValToBuy, } encodedData, err := rlp.EncodeToBytes(data) diff --git a/core/transaction/sell_coin.go b/core/transaction/sell_coin.go index 1744b011c..edaee2935 100644 --- a/core/transaction/sell_coin.go +++ b/core/transaction/sell_coin.go @@ -32,7 +32,7 @@ func (data SellCoinData) TotalSpend(tx *Transaction, context *state.StateDB) (To coin := context.GetStateCoin(data.CoinToBuy).Data() value = formula.CalculatePurchaseReturn(coin.Volume, coin.ReserveBalance, coin.Crr, data.ValueToSell) - if value.Cmp(data.MinimumValueToBuy) == 1 { + if value.Cmp(data.MinimumValueToBuy) == -1 { return nil, nil, nil, &Response{ Code: code.MinimumValueToBuylReached, Log: fmt.Sprintf("You wanted to get minimum %s, but currently you will get %s", data.MinimumValueToBuy.String(), value.String()), @@ -77,7 +77,7 @@ func (data SellCoinData) TotalSpend(tx *Transaction, context *state.StateDB) (To coin := context.GetStateCoin(data.CoinToSell).Data() value = formula.CalculateSaleReturn(coin.Volume, coin.ReserveBalance, coin.Crr, data.ValueToSell) - if value.Cmp(data.MinimumValueToBuy) == 1 { + if value.Cmp(data.MinimumValueToBuy) == -1 { return nil, nil, nil, &Response{ Code: code.MinimumValueToBuylReached, Log: fmt.Sprintf("You wanted to get minimum %s, but currently you will get %s", data.MinimumValueToBuy.String(), value.String()), @@ -133,7 +133,7 @@ func (data SellCoinData) TotalSpend(tx *Transaction, context *state.StateDB) (To value = formula.CalculatePurchaseReturn(coinTo.Volume, coinTo.ReserveBalance, coinTo.Crr, basecoinValue) - if value.Cmp(data.MinimumValueToBuy) == 1 { + if value.Cmp(data.MinimumValueToBuy) == -1 { return nil, nil, nil, &Response{ Code: code.MinimumValueToBuylReached, Log: fmt.Sprintf("You wanted to get minimum %s, but currently you will get %s", data.MinimumValueToBuy.String(), value.String()), diff --git a/core/transaction/sell_coin_test.go b/core/transaction/sell_coin_test.go index 89613fe4b..5d31977c1 100644 --- a/core/transaction/sell_coin_test.go +++ b/core/transaction/sell_coin_test.go @@ -20,10 +20,12 @@ func TestSellCoinTx(t *testing.T) { cState.AddBalance(addr, coin, helpers.BipToPip(big.NewInt(1000000))) + minValToBuy, _ := big.NewInt(0).SetString("957658277688702625", 10) data := SellCoinData{ - CoinToSell: coin, - ValueToSell: helpers.BipToPip(big.NewInt(10)), - CoinToBuy: getTestCoinSymbol(), + CoinToSell: coin, + ValueToSell: helpers.BipToPip(big.NewInt(10)), + CoinToBuy: getTestCoinSymbol(), + MinimumValueToBuy: minValToBuy, } encodedData, err := rlp.EncodeToBytes(data) From a3d559110a66a86fd33b8fd97c8e1e5260d3a03e Mon Sep 17 00:00:00 2001 From: Daniil Lashin Date: Wed, 5 Dec 2018 12:17:00 +0300 Subject: [PATCH 37/49] API changes --- api/address.go | 4 +- api/block.go | 45 +++++++++++++++++++-- api/validators.go | 5 +-- core/transaction/declare_candidacy.go | 2 +- core/transaction/delegate.go | 2 +- core/transaction/switch_candidate_status.go | 4 +- core/transaction/unbond.go | 2 +- core/types/types.go | 10 ++++- 8 files changed, 60 insertions(+), 14 deletions(-) diff --git a/api/address.go b/api/address.go index dafc15ffb..e99d474b4 100644 --- a/api/address.go +++ b/api/address.go @@ -2,6 +2,7 @@ package api import ( "github.com/MinterTeam/minter-go-node/core/types" + "github.com/MinterTeam/minter-go-node/rpc/lib/types" "math/big" ) @@ -13,13 +14,14 @@ type AddressResponse struct { func Address(address types.Address, height int) (*AddressResponse, error) { cState, err := GetStateForHeight(height) if err != nil { - return nil, err + return nil, &rpctypes.RPCError{Code: 404, Message: "State at given height not found", Data: err.Error()} } response := AddressResponse{ Balance: make(map[string]*big.Int), TransactionCount: cState.GetNonce(address), } + balances := cState.GetBalances(address) for k, v := range balances.Data { diff --git a/api/block.go b/api/block.go index 91d6b7115..19c551350 100644 --- a/api/block.go +++ b/api/block.go @@ -1,13 +1,14 @@ package api import ( + "bytes" "encoding/json" "fmt" "github.com/MinterTeam/minter-go-node/core/rewards" "github.com/MinterTeam/minter-go-node/core/transaction" "github.com/MinterTeam/minter-go-node/core/types" + "github.com/MinterTeam/minter-go-node/rpc/lib/types" "github.com/tendermint/tendermint/libs/common" - tmtypes "github.com/tendermint/tendermint/types" "math/big" "time" ) @@ -19,9 +20,9 @@ type BlockResponse struct { NumTxs int64 `json:"num_txs"` TotalTxs int64 `json:"total_txs"` Transactions []BlockTransactionResponse `json:"transactions"` - Precommits []*tmtypes.Vote `json:"precommits"` BlockReward *big.Int `json:"block_reward"` Size int `json:"size"` + Validators []BlockValidatorResponse `json:"validators"` } type BlockTransactionResponse struct { @@ -42,11 +43,16 @@ type BlockTransactionResponse struct { Log string `json:"log,omitempty"` } +type BlockValidatorResponse struct { + Pubkey string `json:"pubkey"` + Signed bool `json:"signed"` +} + func Block(height int64) (*BlockResponse, error) { block, err := client.Block(&height) blockResults, err := client.BlockResults(&height) if err != nil { - return nil, err + return nil, &rpctypes.RPCError{Code: 404, Message: "Block not found", Data: err.Error()} } txs := make([]BlockTransactionResponse, len(block.Block.Data.Txs)) @@ -89,6 +95,37 @@ func Block(height int64) (*BlockResponse, error) { } } + tmValidators, err := client.Validators(&height) + if err != nil { + return nil, &rpctypes.RPCError{Code: 404, Message: "Validators for block not found", Data: err.Error()} + } + + commit, err := client.Commit(&height) + if err != nil { + return nil, &rpctypes.RPCError{Code: 404, Message: "Commit for block not found", Data: err.Error()} + } + + validators := make([]BlockValidatorResponse, len(commit.Commit.Precommits)) + for i, tmval := range tmValidators.Validators { + signed := false + + for _, vote := range commit.Commit.Precommits { + if vote == nil { + continue + } + + if bytes.Equal(vote.ValidatorAddress.Bytes(), tmval.Address.Bytes()) { + signed = true + break + } + } + + validators[i] = BlockValidatorResponse{ + Pubkey: fmt.Sprintf("Mp%x", tmval.PubKey.Bytes()[5:]), + Signed: signed, + } + } + return &BlockResponse{ Hash: block.Block.Hash(), Height: block.Block.Height, @@ -96,8 +133,8 @@ func Block(height int64) (*BlockResponse, error) { NumTxs: block.Block.NumTxs, TotalTxs: block.Block.TotalTxs, Transactions: txs, - Precommits: block.Block.LastCommit.Precommits, BlockReward: rewards.GetRewardForBlock(uint64(height)), Size: len(cdc.MustMarshalBinaryLengthPrefixed(block)), + Validators: validators, }, nil } diff --git a/api/validators.go b/api/validators.go index 9cac97ff7..756c7883b 100644 --- a/api/validators.go +++ b/api/validators.go @@ -1,7 +1,6 @@ package api import ( - "fmt" "github.com/MinterTeam/minter-go-node/core/state" "github.com/MinterTeam/minter-go-node/core/types" "github.com/pkg/errors" @@ -18,7 +17,7 @@ type Stake struct { type CandidateResponse struct { CandidateAddress types.Address `json:"candidate_address"` TotalStake *big.Int `json:"total_stake"` - PubKey string `json:"pub_key"` + PubKey types.Pubkey `json:"pub_key"` Commission uint `json:"commission"` Stakes []Stake `json:"stakes,omitempty"` CreatedAtBlock uint `json:"created_at_block"` @@ -43,7 +42,7 @@ func makeResponseCandidate(c state.Candidate, includeStakes bool) CandidateRespo candidate := CandidateResponse{ CandidateAddress: c.CandidateAddress, TotalStake: c.TotalBipStake, - PubKey: fmt.Sprintf("Mp%x", c.PubKey), + PubKey: c.PubKey, Commission: c.Commission, CreatedAtBlock: c.CreatedAtBlock, Status: c.Status, diff --git a/core/transaction/declare_candidacy.go b/core/transaction/declare_candidacy.go index d34d97ade..889ddb0a7 100644 --- a/core/transaction/declare_candidacy.go +++ b/core/transaction/declare_candidacy.go @@ -17,7 +17,7 @@ const maxCommission = 100 type DeclareCandidacyData struct { Address types.Address `json:"address"` - PubKey []byte `json:"pub_key"` + PubKey types.Pubkey `json:"pub_key"` Commission uint `json:"commission"` Coin types.CoinSymbol `json:"coin"` Stake *big.Int `json:"stake"` diff --git a/core/transaction/delegate.go b/core/transaction/delegate.go index 1ec743606..2f950515a 100644 --- a/core/transaction/delegate.go +++ b/core/transaction/delegate.go @@ -12,7 +12,7 @@ import ( ) type DelegateData struct { - PubKey []byte `json:"pub_key"` + PubKey types.Pubkey `json:"pub_key"` Coin types.CoinSymbol `json:"coin"` Stake *big.Int `json:"stake"` } diff --git a/core/transaction/switch_candidate_status.go b/core/transaction/switch_candidate_status.go index cea139ddb..de632f421 100644 --- a/core/transaction/switch_candidate_status.go +++ b/core/transaction/switch_candidate_status.go @@ -12,7 +12,7 @@ import ( ) type SetCandidateOnData struct { - PubKey []byte `json:"pub_key"` + PubKey types.Pubkey `json:"pub_key"` } func (data SetCandidateOnData) TotalSpend(tx *Transaction, context *state.StateDB) (TotalSpends, []Conversion, *big.Int, *Response) { @@ -97,7 +97,7 @@ func (data SetCandidateOnData) Run(tx *Transaction, context *state.StateDB, isCh } type SetCandidateOffData struct { - PubKey []byte `json:"pub_key"` + PubKey types.Pubkey `json:"pub_key"` } func (data SetCandidateOffData) TotalSpend(tx *Transaction, context *state.StateDB) (TotalSpends, []Conversion, *big.Int, *Response) { diff --git a/core/transaction/unbond.go b/core/transaction/unbond.go index 593a3a264..475608a98 100644 --- a/core/transaction/unbond.go +++ b/core/transaction/unbond.go @@ -14,7 +14,7 @@ import ( const unbondPeriod = 720 // in mainnet will be 518400 (30 days) type UnbondData struct { - PubKey []byte `json:"pub_key"` + PubKey types.Pubkey `json:"pub_key"` Coin types.CoinSymbol `json:"coin"` Value *big.Int `json:"value"` } diff --git a/core/types/types.go b/core/types/types.go index 2c2cdddc3..656b07b28 100644 --- a/core/types/types.go +++ b/core/types/types.go @@ -275,7 +275,15 @@ func (a UnprefixedAddress) MarshalText() ([]byte, error) { type Pubkey []byte func (p Pubkey) String() string { - return string(p[:]) + return fmt.Sprintf("Mp%x", []byte(p)) +} + +func (p Pubkey) MarshalText() ([]byte, error) { + return []byte(p.String()), nil +} + +func (p Pubkey) MarshalJSON() ([]byte, error) { + return []byte(fmt.Sprintf("\"%s\"", p.String())), nil } func (p Pubkey) Compare(p2 Pubkey) int { From b9844decbe2d2c3a384406d4cb3b8322eee0e34b Mon Sep 17 00:00:00 2001 From: Daniil Lashin Date: Wed, 5 Dec 2018 12:18:17 +0300 Subject: [PATCH 38/49] Update docs --- docs/api.rst | 55 +++++++++++++++++++--------------------------------- 1 file changed, 20 insertions(+), 35 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index 44fb47e51..73293202f 100755 --- a/docs/api.rst +++ b/docs/api.rst @@ -252,57 +252,42 @@ Returns block data at given height. "jsonrpc": "2.0", "id": "", "result": { - "hash": "F4D2F2DF68B20275B832B2D1859308509C373523689259CABD17AFC777C0B014", - "height": "387", - "time": "2018-12-03T14:39:41.364276Z", + "hash": "0B1226C12783373BB2FFB451A104FF2BE47F59B8E7B6690B7712AADBA197D2FC", + "height": "9", + "time": "2018-12-05T09:14:57.114925Z", "num_txs": "1", "total_txs": "1", "transactions": [ { - "hash": "Mtc6c6b5008af8077fb0ce817ddb79268d1c66b6b353af76778ca5a264a80069db", - "raw_tx": "f88701018a4d4e540000000000000001aae98a4d4e540000000000000094ee81347211c72524338f9680072af9074433314688a688906bd8b0000084546573748001b845f8431ba098fd9402b0af434f461eecdad89908655c779fb394b7624a0c37198f931f27a1a075e73a04f81e2204d88826ac851b2b3da359e4a9a16ac6c17e992fa0a3de0c48", + "hash": "Mt0e765f48042683160d33c610a90845aeef5f8e0d71cab60e01895f8bd973d614", + "raw_tx": "f8a701018a4d4e540000000000000006b84df84b94ee81347211c72524338f9680072af90744333146a021e1d043c6d9c0bb0929ab8d1dd9f3948de0f5ad7234ce773a501441d204aa9e0a8a4d4e5400000000000000888ac7230489e80000808001b845f8431ca0a7cfaf4ab3b64695380a5fd2f86f5fd29a56c722572dcb1a7fbc49ba8ff1cdc0a06be96fdf026ed7da605cfa1a606c134d99fea51717dbd57997e5e021ef714944", "from": "Mxee81347211c72524338f9680072af90744333146", "nonce": "1", "gas_price": "1", - "type": 1, + "type": 6, "data": { + "address": "Mxee81347211c72524338f9680072af90744333146", + "pub_key": "Mp21e1d043c6d9c0bb0929ab8d1dd9f3948de0f5ad7234ce773a501441d204aa9e", + "commission": "10", "coin": "MNT", - "to": "Mxee81347211c72524338f9680072af90744333146", - "value": "12000000000000000000" + "stake": "10000000000000000000" }, - "payload": "VGVzdA==", + "payload": "", "service_data": "", - "gas": "18", + "gas": "10000", "gas_coin": "MNT", - "gas_used": "18", - "tags": { - "tx.type": "01", - "tx.from": "ee81347211c72524338f9680072af90744333146", - "tx.to": "ee81347211c72524338f9680072af90744333146", - "tx.coin": "MNT" - } + "gas_used": "10000", + "tags": {} } ], - "precommits": [ + "block_reward": "333000000000000000000", + "size": "1230", + "validators": [ { - "type": 2, - "height": "386", - "round": "0", - "timestamp": "2018-12-03T14:39:41.364276Z", - "block_id": { - "hash": "8348B85D729555F0FEC3258EE07B188A38702F5045C1C0E3F0200A59713AA32F", - "parts": { - "total": "1", - "hash": "5816E926E9006AF09D6E77A51A90051B54E2D6FF984A21BE4AAC39A0E0758678" - } - }, - "validator_address": "AB15A084DD592699812E9B22385C1959E7AEFFB8", - "validator_index": "0", - "signature": "V/9uM9ZVUgfh5NdZn0DS4xWubLqJiEAB+J1McBCKW0Kq2X25L4+6mSjEG/hSYNxLGtS+yV22bhxLcc2qvqoDAg==" + "pubkey": "Mpddfadfb15908ed5607c79e66aaf4030ef93363bd1846d64186d52424b1896c83", + "signed": true } - ], - "block_reward": "333000000000000000000", - "size": "1204" + ] } } From ee78dbffec47716fff03ad05143957a1e568f3d9 Mon Sep 17 00:00:00 2001 From: Daniil Lashin Date: Wed, 5 Dec 2018 12:18:39 +0300 Subject: [PATCH 39/49] New seed address --- genesis/genesis.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/genesis/genesis.go b/genesis/genesis.go index 9f9bddc7c..ed2cfeddd 100644 --- a/genesis/genesis.go +++ b/genesis/genesis.go @@ -48,7 +48,7 @@ func GetTestnetGenesis() (*tmtypes.GenesisDoc, error) { }, }, { - Address: types.HexToAddress("Mxfe60014a6e9ac91618f5d1cab3fd58cded61ee99"), + Address: types.HexToAddress("Mx184ac726059e43643e67290666f7b3195093f870"), Balance: map[string]string{ "MNT": helpers.BipToPip(big.NewInt(100000000)).String(), }, From 051fa29023155d185b6c9dc57f80efedac89e219 Mon Sep 17 00:00:00 2001 From: Daniil Lashin Date: Wed, 5 Dec 2018 12:32:23 +0300 Subject: [PATCH 40/49] Update validators api --- api/validators.go | 33 +++++++++------------------------ docs/api.rst | 23 +---------------------- 2 files changed, 10 insertions(+), 46 deletions(-) diff --git a/api/validators.go b/api/validators.go index 756c7883b..2736a0357 100644 --- a/api/validators.go +++ b/api/validators.go @@ -3,7 +3,6 @@ package api import ( "github.com/MinterTeam/minter-go-node/core/state" "github.com/MinterTeam/minter-go-node/core/types" - "github.com/pkg/errors" "math/big" ) @@ -17,7 +16,7 @@ type Stake struct { type CandidateResponse struct { CandidateAddress types.Address `json:"candidate_address"` TotalStake *big.Int `json:"total_stake"` - PubKey types.Pubkey `json:"pub_key"` + PubKey types.Pubkey `json:"pubkey"` Commission uint `json:"commission"` Stakes []Stake `json:"stakes,omitempty"` CreatedAtBlock uint `json:"created_at_block"` @@ -25,17 +24,7 @@ type CandidateResponse struct { } type ValidatorResponse struct { - AccumReward *big.Int `json:"accumulated_reward"` - AbsentTimes int `json:"absent_times"` - Candidate CandidateResponse `json:"candidate"` -} - -func makeResponseValidator(v state.Validator, state *state.StateDB) ValidatorResponse { - return ValidatorResponse{ - AccumReward: v.AccumReward, - AbsentTimes: v.CountAbsentTimes(), - Candidate: makeResponseCandidate(*state.GetStateCandidate(v.PubKey), false), - } + Pubkey types.Pubkey `json:"pubkey"` } func makeResponseCandidate(c state.Candidate, includeStakes bool) CandidateResponse { @@ -65,21 +54,17 @@ func makeResponseCandidate(c state.Candidate, includeStakes bool) CandidateRespo type ResponseValidators []ValidatorResponse -func Validators(height int) (*ResponseValidators, error) { - rState, err := GetStateForHeight(height) - +func Validators(height int64) (*ResponseValidators, error) { + tmVals, err := client.Validators(&height) if err != nil { return nil, err } - vals := rState.GetStateValidators() - if vals == nil { - return nil, errors.New("Validator not found") - } - - var responseValidators ResponseValidators - for _, val := range vals.Data() { - responseValidators = append(responseValidators, makeResponseValidator(val, rState)) + responseValidators := make(ResponseValidators, len(tmVals.Validators)) + for i, val := range tmVals.Validators { + responseValidators[i] = ValidatorResponse{ + Pubkey: val.PubKey.Bytes()[5:], + } } return &responseValidators, nil diff --git a/docs/api.rst b/docs/api.rst index 73293202f..fbe96dd80 100755 --- a/docs/api.rst +++ b/docs/api.rst @@ -113,7 +113,6 @@ found. } } - Validators ^^^^^^^^^^ @@ -130,22 +129,11 @@ Returns list of active validators. "id": "", "result": [ { - "accumulated_reward": 2331000000000000000000, - "absent_times": "0", - "candidate": { - "candidate_address": "Mxee81347211c72524338f9680072af90744333146", - "total_stake": 0, - "pub_key": "Mpe0ba50e3468b07fbbc127840953eb8f4fe57d6e8162df93baefd79f5d5bc2b97", - "commission": "100", - "created_at_block": "1", - "status": 2 - } + "pubkey": "Mpddfadfb15908ed5607c79e66aaf4030ef93363bd1846d64186d52424b1896c83" } ] } - - Address ^^^^^^^ @@ -168,8 +156,6 @@ Returns the balance of given account and the number of outgoing transaction. } } - - | **Result->balance**: Map of balances. CoinSymbol => Balance (in pips). | **Result->transaction_count**: Count of transactions sent from the account. @@ -195,7 +181,6 @@ Sends transaction to the Minter Network. } } - **Result**: Transaction hash. Transaction @@ -236,7 +221,6 @@ Transaction } } - Block ^^^^^ @@ -326,7 +310,6 @@ Returns information about coin. } } - **Result**: - **Coin name** - Name of a coin. Arbitrary string. - **Coin symbol** - Short symbol of a coin. Coin symbol is unique, alphabetic, uppercase, 3 to 10 letters length. @@ -360,10 +343,8 @@ Request params: } } - **Result**: Amount of "to_coin" user should get. - Estimate buy coin ^^^^^^^^^^^^^^^^^ @@ -411,6 +392,4 @@ Return estimate of buy coin transaction } } - - **Result**: Commission in GasCoin. From 4a17b2845238af1ae3ab3e0fd4e5e5da467e9e00 Mon Sep 17 00:00:00 2001 From: Daniil Lashin Date: Wed, 5 Dec 2018 12:57:08 +0300 Subject: [PATCH 41/49] Add proposer to block API --- api/block.go | 7 +++++++ docs/api.rst | 1 + 2 files changed, 8 insertions(+) diff --git a/api/block.go b/api/block.go index 19c551350..c30a79a69 100644 --- a/api/block.go +++ b/api/block.go @@ -22,6 +22,7 @@ type BlockResponse struct { Transactions []BlockTransactionResponse `json:"transactions"` BlockReward *big.Int `json:"block_reward"` Size int `json:"size"` + Proposer types.Pubkey `json:"proposer"` Validators []BlockValidatorResponse `json:"validators"` } @@ -106,6 +107,7 @@ func Block(height int64) (*BlockResponse, error) { } validators := make([]BlockValidatorResponse, len(commit.Commit.Precommits)) + proposer := types.Pubkey{} for i, tmval := range tmValidators.Validators { signed := false @@ -124,6 +126,10 @@ func Block(height int64) (*BlockResponse, error) { Pubkey: fmt.Sprintf("Mp%x", tmval.PubKey.Bytes()[5:]), Signed: signed, } + + if bytes.Equal(tmval.Address.Bytes(), commit.ProposerAddress.Bytes()) { + proposer = tmval.PubKey.Bytes()[5:] + } } return &BlockResponse{ @@ -135,6 +141,7 @@ func Block(height int64) (*BlockResponse, error) { Transactions: txs, BlockReward: rewards.GetRewardForBlock(uint64(height)), Size: len(cdc.MustMarshalBinaryLengthPrefixed(block)), + Proposer: proposer, Validators: validators, }, nil } diff --git a/docs/api.rst b/docs/api.rst index fbe96dd80..5a8735167 100755 --- a/docs/api.rst +++ b/docs/api.rst @@ -266,6 +266,7 @@ Returns block data at given height. ], "block_reward": "333000000000000000000", "size": "1230", + "proposer": "Mpddfadfb15908ed5607c79e66aaf4030ef93363bd1846d64186d52424b1896c83", "validators": [ { "pubkey": "Mpddfadfb15908ed5607c79e66aaf4030ef93363bd1846d64186d52424b1896c83", From c56b9815ec04e822da3954fed041113c3827caaa Mon Sep 17 00:00:00 2001 From: Daniil Lashin Date: Wed, 5 Dec 2018 16:48:44 +0300 Subject: [PATCH 42/49] Add UnbondEvent #148 --- CHANGELOG.md | 1 + core/minter/minter.go | 6 ++++++ core/state/statedb.go | 6 ++++++ eventsdb/amino.go | 2 ++ eventsdb/events.go | 21 +++++++++++++++++++++ 5 files changed, 36 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 482850c45..0a7b4b412 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ BREAKING CHANGES IMPROVEMENT - [logs] Add `log_format` option to config +- [events] Add UnbondEvent ## 0.7.6 *Nov 27th, 2018* diff --git a/core/minter/minter.go b/core/minter/minter.go index 58b4dabd2..c94cdf7a3 100644 --- a/core/minter/minter.go +++ b/core/minter/minter.go @@ -139,6 +139,12 @@ func (app *Blockchain) BeginBlock(req abciTypes.RequestBeginBlock) abciTypes.Res frozenFunds := app.stateDeliver.GetStateFrozenFunds(uint64(req.Header.Height)) if frozenFunds != nil { for _, item := range frozenFunds.List() { + eventsdb.GetCurrent().AddEvent(req.Header.Height, eventsdb.UnbondEvent{ + Address: item.Address, + Amount: item.Value.Bytes(), + Coin: item.Coin, + ValidatorPubKey: item.CandidateKey, + }) app.stateDeliver.AddBalance(item.Address, item.Coin, item.Value) } diff --git a/core/state/statedb.go b/core/state/statedb.go index 8c57cad02..1158aceb2 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -1400,6 +1400,12 @@ func (s *StateDB) ClearStakes(height int64) { candidates.data[i].Stakes = candidates.data[i].Stakes[:MaxDelegatorsPerCandidate] for _, stake := range dropped { + eventsdb.GetCurrent().AddEvent(height, eventsdb.UnbondEvent{ + Address: stake.Owner, + Amount: stake.Value.Bytes(), + Coin: stake.Coin, + ValidatorPubKey: candidate.PubKey, + }) s.AddBalance(stake.Owner, stake.Coin, stake.Value) } } diff --git a/eventsdb/amino.go b/eventsdb/amino.go index 08b9d274f..3a1add759 100644 --- a/eventsdb/amino.go +++ b/eventsdb/amino.go @@ -8,4 +8,6 @@ func RegisterAminoEvents(codec *amino.Codec) { "minter/RewardEvent", nil) codec.RegisterConcrete(SlashEvent{}, "minter/SlashEvent", nil) + codec.RegisterConcrete(UnbondEvent{}, + "minter/UnbondEvent", nil) } diff --git a/eventsdb/events.go b/eventsdb/events.go index 140f9338b..19bd7b684 100644 --- a/eventsdb/events.go +++ b/eventsdb/events.go @@ -94,3 +94,24 @@ func (e SlashEvent) MarshalJSON() ([]byte, error) { ValidatorPubKey: fmt.Sprintf("Mp%x", e.ValidatorPubKey), }) } + +type UnbondEvent struct { + Address types.Address + Amount []byte + Coin types.CoinSymbol + ValidatorPubKey types.Pubkey +} + +func (e UnbondEvent) MarshalJSON() ([]byte, error) { + return json.Marshal(struct { + Address string `json:"address"` + Amount string `json:"amount"` + Coin string `json:"coin"` + ValidatorPubKey string `json:"validator_pub_key"` + }{ + Address: e.Address.String(), + Amount: big.NewInt(0).SetBytes(e.Amount).String(), + Coin: e.Coin.String(), + ValidatorPubKey: fmt.Sprintf("Mp%x", e.ValidatorPubKey), + }) +} From 9217211f442cee7152696c99665bcb3be67e56e5 Mon Sep 17 00:00:00 2001 From: Daniil Lashin Date: Thu, 6 Dec 2018 11:19:14 +0300 Subject: [PATCH 43/49] Update tendermint to v0.27.0 --- CHANGELOG.md | 1 + Gopkg.lock | 24 ++++++++++++------------ Gopkg.toml | 10 +++------- core/state/tree.go | 2 +- 4 files changed, 17 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a7b4b412..4d03e402a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ BREAKING CHANGES - [core] Limit coins supply to 1,000,000,000,000,000 - [core] Set minimal reserve and min/max coin supply in CreateCoin tx - [core] Add MinimumValueToBuy and MaximumValueToSell to convert transactions +- [tendermint] Update to [v0.27.0](https://github.com/tendermint/tendermint/blob/master/CHANGELOG.md#v0270) IMPROVEMENT diff --git a/Gopkg.lock b/Gopkg.lock index 0b1533342..cf85538b1 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -25,13 +25,6 @@ pruneopts = "UT" revision = "67e573d211ace594f1366b4ce9d39726c4b19bd0" -[[projects]] - digest = "1:70d33e721fdb80ae8a8529d87a5e4093acb8f14c513a092fb7d71750cb3a8438" - name = "github.com/danil-lashin/iavl" - packages = ["."] - pruneopts = "UT" - revision = "ff321d758752b411f0fb01f86370395c314bc13a" - [[projects]] digest = "1:ffe9824d294da03b391f44e1ae8281281b4afc1bdaa9588c9097785e3af10cec" name = "github.com/davecgh/go-spew" @@ -383,7 +376,15 @@ version = "v0.14.1" [[projects]] - digest = "1:a38b9d6e0e2b28b5f046d72cec632d2bdbb736ef9f1693665cd7bbafea763b66" + digest = "1:e1cc8dd891e64aab63b0c09f2f12456cbe2cd9cbd9d96dfae3281245f05c2428" + name = "github.com/tendermint/iavl" + packages = ["."] + pruneopts = "UT" + revision = "de0740903a67b624d887f9055d4c60175dcfa758" + version = "v0.12.0" + +[[projects]] + digest = "1:959b42895610ce2f8e6652e3cb006638d59e84217459913e72df09822aa34bbd" name = "github.com/tendermint/tendermint" packages = [ "abci/client", @@ -437,8 +438,8 @@ "version", ] pruneopts = "UT" - revision = "b771798d48c27630fb37b74c8e229da5a8b2042c" - version = "v0.26.4" + revision = "9c236ffd6c56add84f3c17930ae75c26c68d61ec" + version = "v0.27.0" [[projects]] digest = "1:1fdfa9436cc1c11afbdbfb56549e9201a92db0b0a0b7bef0797e7b68c9e8791f" @@ -575,7 +576,6 @@ input-imports = [ "github.com/MinterTeam/go-amino", "github.com/btcsuite/btcd/btcec", - "github.com/danil-lashin/iavl", "github.com/go-kit/kit/log/term", "github.com/gobuffalo/packr", "github.com/gorilla/websocket", @@ -585,11 +585,11 @@ "github.com/stretchr/testify/assert", "github.com/stretchr/testify/require", "github.com/tendermint/go-amino", + "github.com/tendermint/iavl", "github.com/tendermint/tendermint/abci/types", "github.com/tendermint/tendermint/config", "github.com/tendermint/tendermint/crypto", "github.com/tendermint/tendermint/crypto/ed25519", - "github.com/tendermint/tendermint/crypto/encoding/amino", "github.com/tendermint/tendermint/crypto/multisig", "github.com/tendermint/tendermint/crypto/secp256k1", "github.com/tendermint/tendermint/libs/cli/flags", diff --git a/Gopkg.toml b/Gopkg.toml index baa0a53ee..745f9aa96 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -33,21 +33,17 @@ name = "github.com/gobuffalo/packr" version = "=1.11.1" -[[constraint]] - name = "github.com/gorilla/mux" - version = "=1.6.2" - [[constraint]] name = "github.com/rs/cors" version = "^1.6.0" [[constraint]] - name = "github.com/danil-lashin/iavl" - revision = "ff321d758752b411f0fb01f86370395c314bc13a" + name = "github.com/tendermint/iavl" + version = "0.12.0" [[constraint]] name = "github.com/tendermint/tendermint" - version = "=0.26.4" + version = "0.27.0" [[constraint]] name = "github.com/MinterTeam/go-amino" diff --git a/core/state/tree.go b/core/state/tree.go index 1ca82539f..dfaae6a41 100644 --- a/core/state/tree.go +++ b/core/state/tree.go @@ -1,7 +1,7 @@ package state import ( - "github.com/danil-lashin/iavl" + "github.com/tendermint/iavl" dbm "github.com/tendermint/tendermint/libs/db" "sync" ) From 50d04f25ef8c974001221e065228824222b6b0e1 Mon Sep 17 00:00:00 2001 From: Daniil Lashin Date: Thu, 6 Dec 2018 11:25:03 +0300 Subject: [PATCH 44/49] Update docs --- docs/api.rst | 71 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/docs/api.rst b/docs/api.rst index 5a8735167..377217c18 100755 --- a/docs/api.rst +++ b/docs/api.rst @@ -285,6 +285,77 @@ Returns events at given height. curl -s 'localhost:8841/events?height={height}' +.. code-block:: json + + { + "jsonrpc": "2.0", + "id": "", + "result": { + "events": [ + { + "type": "minter/RewardEvent", + "value": { + "role": "DAO", + "address": "Mxee81347211c72524338f9680072af90744333146", + "amount": "367300000000000000000", + "validator_pub_key": "Mp4d7064646661646662313539303865643536303763373965363661616634303330656639333336336264313834366436343138366435323432346231383936633833" + } + }, + { + "type": "minter/RewardEvent", + "value": { + "role": "Developers", + "address": "Mx444c4f1953ea170f74eabef4eee52ed8276a7d5e", + "amount": "367300000000000000000", + "validator_pub_key": "Mp4d7064646661646662313539303865643536303763373965363661616634303330656639333336336264313834366436343138366435323432346231383936633833" + } + }, + { + "type": "minter/RewardEvent", + "value": { + "role": "Validator", + "address": "Mxee81347211c72524338f9680072af90744333146", + "amount": "2938400000000000000000", + "validator_pub_key": "Mp4d7064646661646662313539303865643536303763373965363661616634303330656639333336336264313834366436343138366435323432346231383936633833" + } + } + ] + } + } + +Candidates +^^^^^^^^^^ + +Returns full list of candidates. + +.. code-block:: bash + + curl -s 'localhost:8841/events?height={height}' + +.. code-block:: json + + { + "jsonrpc": "2.0", + "id": "", + "result": [ + { + "candidate_address": "Mxee81347211c72524338f9680072af90744333146", + "total_stake": "1000000000000000000000000", + "pubkey": "Mpddfadfb15908ed5607c79e66aaf4030ef93363bd1846d64186d52424b1896c83", + "commission": "100", + "created_at_block": "1", + "status": 2 + }, + { + "candidate_address": "Mxee81347211c72524338f9680072af90744333146", + "total_stake": "9900000000000000000", + "pubkey": "Mp21e1d043c6d9c0bb0929ab8d1dd9f3948de0f5ad7234ce773a501441d204aa9e", + "commission": "10", + "created_at_block": "9", + "status": 1 + } + ] + } Coin Info ^^^^^^^^^ From 0dce5ea70d87ea609bf9deb605831b5f6d82e593 Mon Sep 17 00:00:00 2001 From: Daniil Lashin Date: Thu, 6 Dec 2018 11:32:23 +0300 Subject: [PATCH 45/49] Add voting power to validators api --- api/candidate.go | 45 +++++++++++++++++++++++++++++++++++++++ api/validators.go | 54 +++++++---------------------------------------- docs/api.rst | 3 ++- 3 files changed, 55 insertions(+), 47 deletions(-) diff --git a/api/candidate.go b/api/candidate.go index 3a2a6f306..aa6df2dc8 100644 --- a/api/candidate.go +++ b/api/candidate.go @@ -1,9 +1,54 @@ package api import ( + "github.com/MinterTeam/minter-go-node/core/state" + "github.com/MinterTeam/minter-go-node/core/types" "github.com/pkg/errors" + "math/big" ) +type Stake struct { + Owner types.Address `json:"owner"` + Coin types.CoinSymbol `json:"coin"` + Value string `json:"value"` + BipValue string `json:"bip_value"` +} + +type CandidateResponse struct { + CandidateAddress types.Address `json:"candidate_address"` + TotalStake *big.Int `json:"total_stake"` + PubKey types.Pubkey `json:"pubkey"` + Commission uint `json:"commission"` + Stakes []Stake `json:"stakes,omitempty"` + CreatedAtBlock uint `json:"created_at_block"` + Status byte `json:"status"` +} + +func makeResponseCandidate(c state.Candidate, includeStakes bool) CandidateResponse { + candidate := CandidateResponse{ + CandidateAddress: c.CandidateAddress, + TotalStake: c.TotalBipStake, + PubKey: c.PubKey, + Commission: c.Commission, + CreatedAtBlock: c.CreatedAtBlock, + Status: c.Status, + } + + if includeStakes { + candidate.Stakes = make([]Stake, len(c.Stakes)) + for i, stake := range c.Stakes { + candidate.Stakes[i] = Stake{ + Owner: stake.Owner, + Coin: stake.Coin, + Value: stake.Value.String(), + BipValue: stake.BipValue.String(), + } + } + } + + return candidate +} + func Candidate(pubkey []byte, height int) (*CandidateResponse, error) { cState, err := GetStateForHeight(height) if err != nil { diff --git a/api/validators.go b/api/validators.go index 2736a0357..9cb3c56d5 100644 --- a/api/validators.go +++ b/api/validators.go @@ -1,60 +1,21 @@ package api import ( - "github.com/MinterTeam/minter-go-node/core/state" "github.com/MinterTeam/minter-go-node/core/types" - "math/big" ) -type Stake struct { - Owner types.Address `json:"owner"` - Coin types.CoinSymbol `json:"coin"` - Value string `json:"value"` - BipValue string `json:"bip_value"` -} - -type CandidateResponse struct { - CandidateAddress types.Address `json:"candidate_address"` - TotalStake *big.Int `json:"total_stake"` - PubKey types.Pubkey `json:"pubkey"` - Commission uint `json:"commission"` - Stakes []Stake `json:"stakes,omitempty"` - CreatedAtBlock uint `json:"created_at_block"` - Status byte `json:"status"` -} - type ValidatorResponse struct { - Pubkey types.Pubkey `json:"pubkey"` -} - -func makeResponseCandidate(c state.Candidate, includeStakes bool) CandidateResponse { - candidate := CandidateResponse{ - CandidateAddress: c.CandidateAddress, - TotalStake: c.TotalBipStake, - PubKey: c.PubKey, - Commission: c.Commission, - CreatedAtBlock: c.CreatedAtBlock, - Status: c.Status, - } - - if includeStakes { - candidate.Stakes = make([]Stake, len(c.Stakes)) - for i, stake := range c.Stakes { - candidate.Stakes[i] = Stake{ - Owner: stake.Owner, - Coin: stake.Coin, - Value: stake.Value.String(), - BipValue: stake.BipValue.String(), - } - } - } - - return candidate + Pubkey types.Pubkey `json:"pubkey"` + VotingPower int64 `json:"voting_power"` } type ResponseValidators []ValidatorResponse func Validators(height int64) (*ResponseValidators, error) { + if height == 0 { + height = blockchain.Height() + } + tmVals, err := client.Validators(&height) if err != nil { return nil, err @@ -63,7 +24,8 @@ func Validators(height int64) (*ResponseValidators, error) { responseValidators := make(ResponseValidators, len(tmVals.Validators)) for i, val := range tmVals.Validators { responseValidators[i] = ValidatorResponse{ - Pubkey: val.PubKey.Bytes()[5:], + Pubkey: val.PubKey.Bytes()[5:], + VotingPower: val.VotingPower, } } diff --git a/docs/api.rst b/docs/api.rst index 377217c18..a164f1002 100755 --- a/docs/api.rst +++ b/docs/api.rst @@ -129,7 +129,8 @@ Returns list of active validators. "id": "", "result": [ { - "pubkey": "Mpddfadfb15908ed5607c79e66aaf4030ef93363bd1846d64186d52424b1896c83" + "pubkey": "Mpddfadfb15908ed5607c79e66aaf4030ef93363bd1846d64186d52424b1896c83", + "voting_power": "100000000" } ] } From ad0bf5bf3010d556097193fc06f7c2f329da8936 Mon Sep 17 00:00:00 2001 From: Daniil Lashin Date: Thu, 6 Dec 2018 11:50:52 +0300 Subject: [PATCH 46/49] Fix GUI --- gui/a_gui-packr.go | 2 +- gui/html/index.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/gui/a_gui-packr.go b/gui/a_gui-packr.go index 0b2b53357..2c2f2e0cb 100644 --- a/gui/a_gui-packr.go +++ b/gui/a_gui-packr.go @@ -7,5 +7,5 @@ import "github.com/gobuffalo/packr" // You can use the "packr clean" command to clean up this, // and any other packr generated files. func init() { - packr.PackJSONBytes("./html", "index.html", "\"PGh0bWw+CjxoZWFkPgogICAgPG1ldGEgY2hhcnNldD0iVVRGLTgiPgogICAgPG1ldGEgbmFtZT0idmlld3BvcnQiCiAgICAgICAgICBjb250ZW50PSJ3aWR0aD1kZXZpY2Utd2lkdGgsIHVzZXItc2NhbGFibGU9bm8sIGluaXRpYWwtc2NhbGU9MS4wLCBtYXhpbXVtLXNjYWxlPTEuMCwgbWluaW11bS1zY2FsZT0xLjAiPgogICAgPG1ldGEgaHR0cC1lcXVpdj0iWC1VQS1Db21wYXRpYmxlIiBjb250ZW50PSJpZT1lZGdlIj4KICAgIDx0aXRsZT5NaW50ZXIgTm9kZSBHVUk8L3RpdGxlPgogICAgPGxpbmsgcmVsPSJzdHlsZXNoZWV0IiBocmVmPSJodHRwczovL3N0YWNrcGF0aC5ib290c3RyYXBjZG4uY29tL2Jvb3RzdHJhcC80LjEuMC9jc3MvYm9vdHN0cmFwLm1pbi5jc3MiCiAgICAgICAgICBpbnRlZ3JpdHk9InNoYTM4NC05Z1ZRNGRZRnd3V1NqSURabkxFV254Q2plU1dGcGhKaXdHUFhyMWpkZEloT2VnaXUxRndPNXFSR3ZGWE9kSlo0IiBjcm9zc29yaWdpbj0iYW5vbnltb3VzIj4KICAgIDxsaW5rIHJlbD0ic3R5bGVzaGVldCIgaHJlZj0iaHR0cHM6Ly91c2UuZm9udGF3ZXNvbWUuY29tL3JlbGVhc2VzL3Y1LjEuMS9jc3MvYWxsLmNzcyIgaW50ZWdyaXR5PSJzaGEzODQtTzh3aFMzZmhHMk9uQTVLYXMwWTlsM2NmcG1ZamFwakkwRTR0aGVINGl1TUQrcExoYmY2SkkwaklNZlljSzN5WiIgY3Jvc3NvcmlnaW49ImFub255bW91cyI+CiAgICA8c2NyaXB0IHNyYz0iaHR0cHM6Ly9jZG4uanNkZWxpdnIubmV0L25wbS92dWUiPjwvc2NyaXB0PgogICAgPHNjcmlwdCBzcmM9Imh0dHBzOi8vY2RuanMuY2xvdWRmbGFyZS5jb20vYWpheC9saWJzL2F4aW9zLzAuMTguMC9heGlvcy5taW4uanMiPjwvc2NyaXB0PgogICAgPHNjcmlwdCBzcmM9Imh0dHBzOi8vY2RuanMuY2xvdWRmbGFyZS5jb20vYWpheC9saWJzL2NyeXB0by1qcy8zLjEuOS0xL2NyeXB0by1qcy5taW4uanMiPjwvc2NyaXB0PgogICAgPHN0eWxlPgoKICAgICAgICAuY2FyZCB7CiAgICAgICAgICAgIG1hcmdpbi1ib3R0b206IDIwcHg7CiAgICAgICAgfQoKICAgICAgICBodG1sLGJvZHksLmJvZHksI2FwcCB7CiAgICAgICAgICAgIG1pbi1oZWlnaHQ6IDEwMCU7CiAgICAgICAgfQogICAgICAgIAogICAgICAgIC5ib2R5IHsKICAgICAgICAgICAgcGFkZGluZy10b3A6IDE1cHg7CiAgICAgICAgfQoKICAgICAgICAudGFibGUgewogICAgICAgICAgICBtYXJnaW4tYm90dG9tOiAwOwogICAgICAgICAgICB0YWJsZS1sYXlvdXQ6IGZpeGVkOwogICAgICAgIH0KCiAgICAgICAgLmNhcmQtaGVhZGVyIHsKICAgICAgICAgICAgZm9udC13ZWlnaHQ6IGJvbGQ7CiAgICAgICAgfQoKICAgICAgICAuY2FyZC1oZWFkZXIgewogICAgICAgICAgICBwYWRkaW5nLWxlZnQ6IDEycHg7CiAgICAgICAgfQoKICAgICAgICAuaCB7CiAgICAgICAgICAgIHdpZHRoOiAyMDBweDsKICAgICAgICAgICAgYmFja2dyb3VuZC1jb2xvcjogI2YzZjNmMzsKICAgICAgICAgICAgYm9yZGVyLXJpZ2h0OiAxcHggc29saWQgI2NjYzsKICAgICAgICB9CgogICAgICAgIC5iZy1zdWNjZXNzLCAuYmctZGFuZ2VyIHsKICAgICAgICAgICAgY29sb3I6IHdoaXRlOwogICAgICAgIH0KCiAgICAgICAgLmJnLWRhbmdlciB7CiAgICAgICAgICAgIGJvcmRlci1jb2xvcjogI2RjMzU0NSAhaW1wb3J0YW50OwogICAgICAgIH0KCiAgICAgICAgLmJnLXN1Y2Nlc3MgewogICAgICAgICAgICBib3JkZXItY29sb3I6ICMyOGE3NDUgIWltcG9ydGFudDsKICAgICAgICB9CgogICAgICAgIC5mYS1jaGVjayB7CiAgICAgICAgICAgIGNvbG9yOiBncmVlbjsKICAgICAgICB9CgogICAgICAgIC5mYS1leGNsYW1hdGlvbi1jaXJjbGUgewogICAgICAgICAgICBjb2xvcjogcmVkOwogICAgICAgIH0KICAgIDwvc3R5bGU+CjwvaGVhZD4KPGJvZHkgc3R5bGU9ImJhY2tncm91bmQtY29sb3I6ICMzNDNhNDAxYSI+CjxkaXYgaWQ9ImFwcCI+CiAgICA8bmF2IGNsYXNzPSJuYXZiYXIgbmF2YmFyLWV4cGFuZC1sZyBuYXZiYXItZGFyayBiZy1kYXJrIj4KICAgICAgICA8ZGl2IGNsYXNzPSJjb250YWluZXIiPgogICAgICAgICAgICA8c3BhbiBjbGFzcz0ibmF2YmFyLWJyYW5kIG1iLTAgaDEiPjxpIGNsYXNzPSJmYXMgZmEtdGVybWluYWwiPjwvaT4gJm5ic3A7IE1pbnRlciBGdWxsIE5vZGUgU3RhdHVzPC9zcGFuPgogICAgICAgIDwvZGl2PgogICAgPC9uYXY+CiAgICA8ZGl2IGNsYXNzPSJjb250YWluZXIgYm9keSIgdi1pZj0iZXJyb3IiPgogICAgICAgIDxkaXYgY2xhc3M9ImFsZXJ0IGFsZXJ0LWRhbmdlciIgcm9sZT0iYWxlcnQiPgogICAgICAgICAgICA8aDQgY2xhc3M9ImFsZXJ0LWhlYWRpbmciPkVycm9yIHdoaWxlIGNvbm5lY3RpbmcgdG8gbG9jYWwgbm9kZTwvaDQ+CiAgICAgICAgICAgIDxwIGNsYXNzPSJtYi0wIj57eyBlcnJvciB9fTwvcD4KICAgICAgICA8L2Rpdj4KICAgIDwvZGl2PgogICAgPGRpdiBjbGFzcz0iY29udGFpbmVyIGJvZHkgYmctd2hpdGUiIHYtaWY9InN0YXR1cyAmJiAhZXJyb3IiPgogICAgICAgIDxkaXYgY2xhc3M9InJvdyI+CiAgICAgICAgICAgIDxkaXYgY2xhc3M9ImNvbCI+CiAgICAgICAgICAgICAgICA8ZGl2IGNsYXNzPSJjYXJkIj4KICAgICAgICAgICAgICAgICAgICA8ZGl2IGNsYXNzPSJjYXJkLWhlYWRlciI+CiAgICAgICAgICAgICAgICAgICAgICAgIE5vZGUgSW5mbwogICAgICAgICAgICAgICAgICAgIDwvZGl2PgogICAgICAgICAgICAgICAgICAgIDx0YWJsZSBjbGFzcz0idGFibGUgY2FyZC1ib2R5Ij4KICAgICAgICAgICAgICAgICAgICAgICAgPHRib2R5PgogICAgICAgICAgICAgICAgICAgICAgICA8dHI+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dGQgY2xhc3M9ImgiPk1vbmlrZXI8L3RkPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgPHRkPnt7IHN0YXR1cy5ub2RlX2luZm8ubW9uaWtlciB9fTwvdGQ+CiAgICAgICAgICAgICAgICAgICAgICAgIDwvdHI+CiAgICAgICAgICAgICAgICAgICAgICAgIDx0cj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0ZCBjbGFzcz0iaCI+Tm9kZSBJRDwvdGQ+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dGQ+e3sgc3RhdHVzLm5vZGVfaW5mby5pZCB9fTwvdGQ+CiAgICAgICAgICAgICAgICAgICAgICAgIDwvdHI+CiAgICAgICAgICAgICAgICAgICAgICAgIDx0cj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0ZCBjbGFzcz0iaCI+TmV0d29yayBJRDwvdGQ+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dGQ+e3sgc3RhdHVzLm5vZGVfaW5mby5uZXR3b3JrIH19PC90ZD4KICAgICAgICAgICAgICAgICAgICAgICAgPC90cj4KICAgICAgICAgICAgICAgICAgICAgICAgPHRyPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgPHRkIGNsYXNzPSJoIj5NaW50ZXIgVmVyc2lvbjwvdGQ+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dGQ+e3sgdmVyc2lvbiB9fTwvdGQ+CiAgICAgICAgICAgICAgICAgICAgICAgIDwvdHI+CiAgICAgICAgICAgICAgICAgICAgICAgIDx0cj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0ZCBjbGFzcz0iaCI+VGVuZGVybWludCBWZXJzaW9uPC90ZD4KICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0ZD57eyBzdGF0dXMubm9kZV9pbmZvLnZlcnNpb24gfX08L3RkPgogICAgICAgICAgICAgICAgICAgICAgICA8L3RyPgogICAgICAgICAgICAgICAgICAgICAgICA8L3Rib2R5PgogICAgICAgICAgICAgICAgICAgIDwvdGFibGU+CiAgICAgICAgICAgICAgICA8L2Rpdj4KICAgICAgICAgICAgICAgIDxkaXYgY2xhc3M9ImNhcmQiIHYtaWY9Im5ldF9pbmZvIj4KICAgICAgICAgICAgICAgICAgICA8ZGl2IGNsYXNzPSJjYXJkLWhlYWRlciI+CiAgICAgICAgICAgICAgICAgICAgICAgIE5ldCBJbmZvCiAgICAgICAgICAgICAgICAgICAgPC9kaXY+CiAgICAgICAgICAgICAgICAgICAgPHRhYmxlIGNsYXNzPSJ0YWJsZSBjYXJkLWJvZHkiPgogICAgICAgICAgICAgICAgICAgICAgICA8dGJvZHk+CiAgICAgICAgICAgICAgICAgICAgICAgIDx0cj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0ZCBjbGFzcz0iaCI+SXMgTGlzdGVuaW5nPC90ZD4KICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0ZD48aSA6Y2xhc3M9InsnZmEtY2hlY2snOiBuZXRfaW5mby5saXN0ZW5pbmd9IiBjbGFzcz0iZmFzIj48L2k+PC90ZD4KICAgICAgICAgICAgICAgICAgICAgICAgPC90cj4KICAgICAgICAgICAgICAgICAgICAgICAgPHRyPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgPHRkIGNsYXNzPSJoIj5Db25uZWN0ZWQgUGVlcnM8L3RkPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgPHRkPnt7IG5ldF9pbmZvLm5fcGVlcnMgfX0gPGkgOmNsYXNzPSJ7J2ZhLWV4Y2xhbWF0aW9uLWNpcmNsZSc6IG5ldF9pbmZvLm5fcGVlcnMgPCAxfSIgY2xhc3M9ImZhcyI+PC9pPjwvdGQ+CiAgICAgICAgICAgICAgICAgICAgICAgIDwvdHI+CiAgICAgICAgICAgICAgICAgICAgICAgIDwvdGJvZHk+CiAgICAgICAgICAgICAgICAgICAgPC90YWJsZT4KICAgICAgICAgICAgICAgIDwvZGl2PgogICAgICAgICAgICA8L2Rpdj4KICAgICAgICAgICAgPGRpdiBjbGFzcz0iY29sIj4KICAgICAgICAgICAgICAgIDxkaXYgY2xhc3M9ImNhcmQiPgogICAgICAgICAgICAgICAgICAgIDxkaXYgY2xhc3M9ImNhcmQtaGVhZGVyIj4KICAgICAgICAgICAgICAgICAgICAgICAgU3luY2luZyBJbmZvCiAgICAgICAgICAgICAgICAgICAgPC9kaXY+CiAgICAgICAgICAgICAgICAgICAgPHRhYmxlIGNsYXNzPSJ0YWJsZSBjYXJkLWJvZHkiPgogICAgICAgICAgICAgICAgICAgICAgICA8dGJvZHk+CiAgICAgICAgICAgICAgICAgICAgICAgIDx0cj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0ZCBjbGFzcz0iaCI+SXMgU3luY2VkPC90ZD4KICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0ZD4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8c3BhbiB2LWlmPSJzdGF0dXMuc3luY19pbmZvLmNhdGNoaW5nX3VwIj5Obzwvc3Bhbj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8c3BhbiB2LWlmPSIhc3RhdHVzLnN5bmNfaW5mby5jYXRjaGluZ191cCI+WWVzPC9zcGFuPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDxpIDpjbGFzcz0ieydmYS1jaGVjayc6ICFzdGF0dXMuc3luY19pbmZvLmNhdGNoaW5nX3VwLCAnZmEtZXhjbGFtYXRpb24tY2lyY2xlJzogc3RhdHVzLnN5bmNfaW5mby5jYXRjaGluZ191cH0iIGNsYXNzPSJmYXMiPjwvaT48L3RkPgogICAgICAgICAgICAgICAgICAgICAgICA8L3RyPgogICAgICAgICAgICAgICAgICAgICAgICA8dHI+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dGQgY2xhc3M9ImgiPkxhdGVzdCBCbG9jayBIZWlnaHQ8L3RkPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgPHRkPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICN7eyBzdGF0dXMuc3luY19pbmZvLmxhdGVzdF9ibG9ja19oZWlnaHQgfX0gPHNwYW4gdi1pZj0ibWFzdGVyU3RhdHVzICYmIE51bWJlcihzdGF0dXMuc3luY19pbmZvLmxhdGVzdF9ibG9ja19oZWlnaHQpIDw9IE51bWJlcihtYXN0ZXJTdGF0dXMubGF0ZXN0X2Jsb2NrX2hlaWdodCkiIGNsYXNzPSJ0ZXh0LW11dGVkIj5vZiB7eyBtYXN0ZXJTdGF0dXMubGF0ZXN0X2Jsb2NrX2hlaWdodCB9fTwvc3Bhbj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvdGQ+CiAgICAgICAgICAgICAgICAgICAgICAgIDwvdHI+CiAgICAgICAgICAgICAgICAgICAgICAgIDx0cj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0ZCBjbGFzcz0iaCI+TGF0ZXN0IEJsb2NrIFRpbWU8L3RkPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgPHRkPgogICAgICAgICAgICAgICAgICAgICAgICAgICAge3sgc3RhdHVzLnN5bmNfaW5mby5sYXRlc3RfYmxvY2tfdGltZSB9fQogICAgICAgICAgICAgICAgICAgICAgICAgICAgPC90ZD4KICAgICAgICAgICAgICAgICAgICAgICAgPC90cj4KICAgICAgICAgICAgICAgICAgICAgICAgPC90Ym9keT4KICAgICAgICAgICAgICAgICAgICA8L3RhYmxlPgogICAgICAgICAgICAgICAgPC9kaXY+CiAgICAgICAgICAgICAgICA8ZGl2IGNsYXNzPSJjYXJkIj4KICAgICAgICAgICAgICAgICAgICA8ZGl2IGNsYXNzPSJjYXJkLWhlYWRlciI+CiAgICAgICAgICAgICAgICAgICAgICAgIFZhbGlkYXRvciBJbmZvCiAgICAgICAgICAgICAgICAgICAgPC9kaXY+CiAgICAgICAgICAgICAgICAgICAgPHRhYmxlIGNsYXNzPSJ0YWJsZSBjYXJkLWJvZHkiPgogICAgICAgICAgICAgICAgICAgICAgICA8dGJvZHk+CiAgICAgICAgICAgICAgICAgICAgICAgIDx0cj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0ZD5QdWJsaWMgS2V5PC90ZD4KICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0ZD57eyB2YWxpZGF0b3JQdWJLZXkgfX08L3RkPgogICAgICAgICAgICAgICAgICAgICAgICA8L3RyPgogICAgICAgICAgICAgICAgICAgICAgICA8dHI+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dGQ+U3RhdHVzPC90ZD4KICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0ZD57eyB2YWxpZGF0b3JTdGF0dXMgfX08L3RkPgogICAgICAgICAgICAgICAgICAgICAgICA8L3RyPgogICAgICAgICAgICAgICAgICAgICAgICA8dHI+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dGQ+VG90YWwgU3Rha2U8L3RkPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgPHRkPnt7IHN0YWtlIH19IE1OVDwvdGQ+CiAgICAgICAgICAgICAgICAgICAgICAgIDwvdHI+CiAgICAgICAgICAgICAgICAgICAgICAgIDx0cj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0ZD5Wb3RpbmcgUG93ZXI8L3RkPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgPHRkPnt7IG5pY2VOdW0oc3RhdHVzLnZhbGlkYXRvcl9pbmZvLnZvdGluZ19wb3dlcikgfX0gPHNwYW4gY2xhc3M9InRleHQtbXV0ZWQiPm9mIDEwMCwwMDAsMDAwPC9zcGFuPjwvdGQ+CiAgICAgICAgICAgICAgICAgICAgICAgIDwvdHI+CiAgICAgICAgICAgICAgICAgICAgICAgIDwvdGJvZHk+CiAgICAgICAgICAgICAgICAgICAgPC90YWJsZT4KICAgICAgICAgICAgICAgIDwvZGl2PgogICAgICAgICAgICA8L2Rpdj4KICAgICAgICA8L2Rpdj4KICAgIDwvZGl2Pgo8L2Rpdj4KPHNjcmlwdD4KICAgIG5ldyBWdWUoewogICAgICAgIGVsOiAnI2FwcCcsCiAgICAgICAgZGF0YTogewogICAgICAgICAgICBtYXN0ZXJTdGF0dXM6IG51bGwsCiAgICAgICAgICAgIHN0YXR1czogbnVsbCwKICAgICAgICAgICAgdmVyc2lvbjogbnVsbCwKICAgICAgICAgICAgbmV0X2luZm86IG51bGwsCiAgICAgICAgICAgIGVycm9yOiBudWxsLAogICAgICAgICAgICB2YWxpZGF0b3JQdWJLZXk6ICcuLi4nLAogICAgICAgICAgICB2YWxpZGF0b3JTdGF0dXM6ICcuLi4nLAogICAgICAgICAgICBzdGFrZTogJy4uLicKICAgICAgICB9LAogICAgICAgIG1vdW50ZWQoKSB7CiAgICAgICAgICAgIHRoaXMucmVmcmVzaCgpCiAgICAgICAgfSwKICAgICAgICBtZXRob2RzOiB7CiAgICAgICAgICAgIG5pY2VOdW0obnVtKSB7CiAgICAgICAgICAgICAgICByZXR1cm4gbnVtLnRvU3RyaW5nKCkucmVwbGFjZSgvXEIoPz0oXGR7M30pKyg/IVxkKSkvZywgIiwiKQogICAgICAgICAgICB9LAogICAgICAgICAgICBiYXNlNjRUb0hleChiYXNlNjQpIHsKICAgICAgICAgICAgICAgIHJldHVybiBDcnlwdG9KUy5lbmMuQmFzZTY0LnBhcnNlKGJhc2U2NCkudG9TdHJpbmcoKQogICAgICAgICAgICB9LAogICAgICAgICAgICByZWZyZXNoKCkgewogICAgICAgICAgICAgICAgYXhpb3MuYWxsKFsKICAgICAgICAgICAgICAgICAgICBheGlvcy5nZXQoIi8vIiArIHdpbmRvdy5sb2NhdGlvbi5ob3N0bmFtZSArICc6ODg0MS9zdGF0dXMnKSwKICAgICAgICAgICAgICAgICAgICBheGlvcy5nZXQoIi8vIiArIHdpbmRvdy5sb2NhdGlvbi5ob3N0bmFtZSArICc6ODg0MS9uZXRfaW5mbycpLAogICAgICAgICAgICAgICAgXSkudGhlbihheGlvcy5zcHJlYWQoZnVuY3Rpb24gKHN0YXR1cywgbmV0X2luZm8pIHsKICAgICAgICAgICAgICAgICAgICB0aGlzLmVycm9yID0gbnVsbAoKICAgICAgICAgICAgICAgICAgICB0aGlzLnN0YXR1cyA9IHN0YXR1cy5kYXRhLnJlc3VsdC50bV9zdGF0dXMKICAgICAgICAgICAgICAgICAgICB0aGlzLnZlcnNpb24gPSBzdGF0dXMuZGF0YS5yZXN1bHQudmVyc2lvbgogICAgICAgICAgICAgICAgICAgIHRoaXMubmV0X2luZm8gPSBuZXRfaW5mby5kYXRhLnJlc3VsdAoKICAgICAgICAgICAgICAgICAgICB0aGlzLnZhbGlkYXRvclB1YktleSA9ICdNcCcgKyB0aGlzLmJhc2U2NFRvSGV4KHN0YXR1cy5kYXRhLnJlc3VsdC50bV9zdGF0dXMudmFsaWRhdG9yX2luZm8ucHViX2tleS52YWx1ZSkKCiAgICAgICAgICAgICAgICAgICAgYXhpb3MuYWxsKFsKICAgICAgICAgICAgICAgICAgICAgICAgYXhpb3MuZ2V0KCIvLyIgKyB3aW5kb3cubG9jYXRpb24uaG9zdG5hbWUgKyAnOjg4NDEvdmFsaWRhdG9ycycpLAogICAgICAgICAgICAgICAgICAgICAgICBheGlvcy5nZXQoIi8vIiArIHdpbmRvdy5sb2NhdGlvbi5ob3N0bmFtZSArICc6ODg0MS9jYW5kaWRhdGU/cHVia2V5PScgKyB0aGlzLnZhbGlkYXRvclB1YktleSksCiAgICAgICAgICAgICAgICAgICAgXSkudGhlbihheGlvcy5zcHJlYWQoZnVuY3Rpb24gKHZhbGlkYXRvcnMsIGNhbmRpZGF0ZSkgewoKICAgICAgICAgICAgICAgICAgICAgICAgdGhpcy5zdGFrZSA9IE1hdGgucm91bmQoY2FuZGlkYXRlLmRhdGEucmVzdWx0LnRvdGFsX3N0YWtlIC8gTWF0aC5wb3coMTAsIDE3KSkgLyAxMAoKICAgICAgICAgICAgICAgICAgICAgICAgaWYgKHZhbGlkYXRvcnMuZGF0YS5yZXN1bHQuZmluZChmdW5jdGlvbih2YWwpIHsgcmV0dXJuIHZhbC5jYW5kaWRhdGUucHViX2tleSA9PT0gdGhpcy52YWxpZGF0b3JQdWJLZXkgfS5iaW5kKHRoaXMpKSkgewogICAgICAgICAgICAgICAgICAgICAgICAgICAgdGhpcy52YWxpZGF0b3JTdGF0dXMgPSAnVmFsaWRhdGluZyc7CiAgICAgICAgICAgICAgICAgICAgICAgICAgICByZXR1cm4KICAgICAgICAgICAgICAgICAgICAgICAgfQoKICAgICAgICAgICAgICAgICAgICAgICAgaWYgKGNhbmRpZGF0ZS5kYXRhLnJlc3VsdC5zdGF0dXMgPT09IDIpIHsKICAgICAgICAgICAgICAgICAgICAgICAgICAgIHRoaXMudmFsaWRhdG9yU3RhdHVzID0gJ0NhbmRpZGF0ZScKICAgICAgICAgICAgICAgICAgICAgICAgICAgIHJldHVybgogICAgICAgICAgICAgICAgICAgICAgICB9CgogICAgICAgICAgICAgICAgICAgICAgICB0aGlzLnZhbGlkYXRvclN0YXR1cyA9ICdEb3duJzsKICAgICAgICAgICAgICAgICAgICB9LmJpbmQodGhpcykpKS5jYXRjaChmdW5jdGlvbigpICB7CiAgICAgICAgICAgICAgICAgICAgICAgIHRoaXMudmFsaWRhdG9yU3RhdHVzID0gJ05vdCBkZWNsYXJlZCc7CiAgICAgICAgICAgICAgICAgICAgICAgIHRoaXMuc3Rha2UgPSAwOwogICAgICAgICAgICAgICAgICAgIH0uYmluZCh0aGlzKSk7CgogICAgICAgICAgICAgICAgICAgIHNldFRpbWVvdXQodGhpcy5yZWZyZXNoLCA1MDAwKQogICAgICAgICAgICAgICAgfS5iaW5kKHRoaXMpKSkuY2F0Y2goZnVuY3Rpb24gKHJlYXNvbikgewogICAgICAgICAgICAgICAgICAgIHRoaXMuZXJyb3IgPSByZWFzb24udG9TdHJpbmcoKTsKICAgICAgICAgICAgICAgICAgICBzZXRUaW1lb3V0KHRoaXMucmVmcmVzaCwgNTAwMCkKICAgICAgICAgICAgICAgIH0uYmluZCh0aGlzKSkKCiAgICAgICAgICAgICAgICBheGlvcy5nZXQoImh0dHBzOi8vbWludGVyLW5vZGUtMS50ZXN0bmV0Lm1pbnRlci5uZXR3b3JrL3N0YXR1cyIpLnRoZW4oZnVuY3Rpb24gKG1hc3RlclN0YXR1cykgewogICAgICAgICAgICAgICAgICAgIHRoaXMubWFzdGVyU3RhdHVzID0gbWFzdGVyU3RhdHVzLmRhdGEucmVzdWx0CiAgICAgICAgICAgICAgICB9LmJpbmQodGhpcykpCiAgICAgICAgICAgIH0KICAgICAgICB9CiAgICB9KQo8L3NjcmlwdD4KPC9ib2R5Pgo8L2h0bWw+\"") + packr.PackJSONBytes("./html", "index.html", "\"PGh0bWw+CjxoZWFkPgogICAgPG1ldGEgY2hhcnNldD0iVVRGLTgiPgogICAgPG1ldGEgbmFtZT0idmlld3BvcnQiCiAgICAgICAgICBjb250ZW50PSJ3aWR0aD1kZXZpY2Utd2lkdGgsIHVzZXItc2NhbGFibGU9bm8sIGluaXRpYWwtc2NhbGU9MS4wLCBtYXhpbXVtLXNjYWxlPTEuMCwgbWluaW11bS1zY2FsZT0xLjAiPgogICAgPG1ldGEgaHR0cC1lcXVpdj0iWC1VQS1Db21wYXRpYmxlIiBjb250ZW50PSJpZT1lZGdlIj4KICAgIDx0aXRsZT5NaW50ZXIgTm9kZSBHVUk8L3RpdGxlPgogICAgPGxpbmsgcmVsPSJzdHlsZXNoZWV0IiBocmVmPSJodHRwczovL3N0YWNrcGF0aC5ib290c3RyYXBjZG4uY29tL2Jvb3RzdHJhcC80LjEuMC9jc3MvYm9vdHN0cmFwLm1pbi5jc3MiCiAgICAgICAgICBpbnRlZ3JpdHk9InNoYTM4NC05Z1ZRNGRZRnd3V1NqSURabkxFV254Q2plU1dGcGhKaXdHUFhyMWpkZEloT2VnaXUxRndPNXFSR3ZGWE9kSlo0IiBjcm9zc29yaWdpbj0iYW5vbnltb3VzIj4KICAgIDxsaW5rIHJlbD0ic3R5bGVzaGVldCIgaHJlZj0iaHR0cHM6Ly91c2UuZm9udGF3ZXNvbWUuY29tL3JlbGVhc2VzL3Y1LjEuMS9jc3MvYWxsLmNzcyIgaW50ZWdyaXR5PSJzaGEzODQtTzh3aFMzZmhHMk9uQTVLYXMwWTlsM2NmcG1ZamFwakkwRTR0aGVINGl1TUQrcExoYmY2SkkwaklNZlljSzN5WiIgY3Jvc3NvcmlnaW49ImFub255bW91cyI+CiAgICA8c2NyaXB0IHNyYz0iaHR0cHM6Ly9jZG4uanNkZWxpdnIubmV0L25wbS92dWUiPjwvc2NyaXB0PgogICAgPHNjcmlwdCBzcmM9Imh0dHBzOi8vY2RuanMuY2xvdWRmbGFyZS5jb20vYWpheC9saWJzL2F4aW9zLzAuMTguMC9heGlvcy5taW4uanMiPjwvc2NyaXB0PgogICAgPHNjcmlwdCBzcmM9Imh0dHBzOi8vY2RuanMuY2xvdWRmbGFyZS5jb20vYWpheC9saWJzL2NyeXB0by1qcy8zLjEuOS0xL2NyeXB0by1qcy5taW4uanMiPjwvc2NyaXB0PgogICAgPHN0eWxlPgoKICAgICAgICAuY2FyZCB7CiAgICAgICAgICAgIG1hcmdpbi1ib3R0b206IDIwcHg7CiAgICAgICAgfQoKICAgICAgICBodG1sLGJvZHksLmJvZHksI2FwcCB7CiAgICAgICAgICAgIG1pbi1oZWlnaHQ6IDEwMCU7CiAgICAgICAgfQogICAgICAgIAogICAgICAgIC5ib2R5IHsKICAgICAgICAgICAgcGFkZGluZy10b3A6IDE1cHg7CiAgICAgICAgfQoKICAgICAgICAudGFibGUgewogICAgICAgICAgICBtYXJnaW4tYm90dG9tOiAwOwogICAgICAgICAgICB0YWJsZS1sYXlvdXQ6IGZpeGVkOwogICAgICAgIH0KCiAgICAgICAgLmNhcmQtaGVhZGVyIHsKICAgICAgICAgICAgZm9udC13ZWlnaHQ6IGJvbGQ7CiAgICAgICAgfQoKICAgICAgICAuY2FyZC1oZWFkZXIgewogICAgICAgICAgICBwYWRkaW5nLWxlZnQ6IDEycHg7CiAgICAgICAgfQoKICAgICAgICAuaCB7CiAgICAgICAgICAgIHdpZHRoOiAyMDBweDsKICAgICAgICAgICAgYmFja2dyb3VuZC1jb2xvcjogI2YzZjNmMzsKICAgICAgICAgICAgYm9yZGVyLXJpZ2h0OiAxcHggc29saWQgI2NjYzsKICAgICAgICB9CgogICAgICAgIC5iZy1zdWNjZXNzLCAuYmctZGFuZ2VyIHsKICAgICAgICAgICAgY29sb3I6IHdoaXRlOwogICAgICAgIH0KCiAgICAgICAgLmJnLWRhbmdlciB7CiAgICAgICAgICAgIGJvcmRlci1jb2xvcjogI2RjMzU0NSAhaW1wb3J0YW50OwogICAgICAgIH0KCiAgICAgICAgLmJnLXN1Y2Nlc3MgewogICAgICAgICAgICBib3JkZXItY29sb3I6ICMyOGE3NDUgIWltcG9ydGFudDsKICAgICAgICB9CgogICAgICAgIC5mYS1jaGVjayB7CiAgICAgICAgICAgIGNvbG9yOiBncmVlbjsKICAgICAgICB9CgogICAgICAgIC5mYS1leGNsYW1hdGlvbi1jaXJjbGUgewogICAgICAgICAgICBjb2xvcjogcmVkOwogICAgICAgIH0KICAgIDwvc3R5bGU+CjwvaGVhZD4KPGJvZHkgc3R5bGU9ImJhY2tncm91bmQtY29sb3I6ICMzNDNhNDAxYSI+CjxkaXYgaWQ9ImFwcCI+CiAgICA8bmF2IGNsYXNzPSJuYXZiYXIgbmF2YmFyLWV4cGFuZC1sZyBuYXZiYXItZGFyayBiZy1kYXJrIj4KICAgICAgICA8ZGl2IGNsYXNzPSJjb250YWluZXIiPgogICAgICAgICAgICA8c3BhbiBjbGFzcz0ibmF2YmFyLWJyYW5kIG1iLTAgaDEiPjxpIGNsYXNzPSJmYXMgZmEtdGVybWluYWwiPjwvaT4gJm5ic3A7IE1pbnRlciBGdWxsIE5vZGUgU3RhdHVzPC9zcGFuPgogICAgICAgIDwvZGl2PgogICAgPC9uYXY+CiAgICA8ZGl2IGNsYXNzPSJjb250YWluZXIgYm9keSIgdi1pZj0iZXJyb3IiPgogICAgICAgIDxkaXYgY2xhc3M9ImFsZXJ0IGFsZXJ0LWRhbmdlciIgcm9sZT0iYWxlcnQiPgogICAgICAgICAgICA8aDQgY2xhc3M9ImFsZXJ0LWhlYWRpbmciPkVycm9yIHdoaWxlIGNvbm5lY3RpbmcgdG8gbG9jYWwgbm9kZTwvaDQ+CiAgICAgICAgICAgIDxwIGNsYXNzPSJtYi0wIj57eyBlcnJvciB9fTwvcD4KICAgICAgICA8L2Rpdj4KICAgIDwvZGl2PgogICAgPGRpdiBjbGFzcz0iY29udGFpbmVyIGJvZHkgYmctd2hpdGUiIHYtaWY9InN0YXR1cyAmJiAhZXJyb3IiPgogICAgICAgIDxkaXYgY2xhc3M9InJvdyI+CiAgICAgICAgICAgIDxkaXYgY2xhc3M9ImNvbCI+CiAgICAgICAgICAgICAgICA8ZGl2IGNsYXNzPSJjYXJkIj4KICAgICAgICAgICAgICAgICAgICA8ZGl2IGNsYXNzPSJjYXJkLWhlYWRlciI+CiAgICAgICAgICAgICAgICAgICAgICAgIE5vZGUgSW5mbwogICAgICAgICAgICAgICAgICAgIDwvZGl2PgogICAgICAgICAgICAgICAgICAgIDx0YWJsZSBjbGFzcz0idGFibGUgY2FyZC1ib2R5Ij4KICAgICAgICAgICAgICAgICAgICAgICAgPHRib2R5PgogICAgICAgICAgICAgICAgICAgICAgICA8dHI+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dGQgY2xhc3M9ImgiPk1vbmlrZXI8L3RkPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgPHRkPnt7IHN0YXR1cy5ub2RlX2luZm8ubW9uaWtlciB9fTwvdGQ+CiAgICAgICAgICAgICAgICAgICAgICAgIDwvdHI+CiAgICAgICAgICAgICAgICAgICAgICAgIDx0cj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0ZCBjbGFzcz0iaCI+Tm9kZSBJRDwvdGQ+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dGQ+e3sgc3RhdHVzLm5vZGVfaW5mby5pZCB9fTwvdGQ+CiAgICAgICAgICAgICAgICAgICAgICAgIDwvdHI+CiAgICAgICAgICAgICAgICAgICAgICAgIDx0cj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0ZCBjbGFzcz0iaCI+TmV0d29yayBJRDwvdGQ+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dGQ+e3sgc3RhdHVzLm5vZGVfaW5mby5uZXR3b3JrIH19PC90ZD4KICAgICAgICAgICAgICAgICAgICAgICAgPC90cj4KICAgICAgICAgICAgICAgICAgICAgICAgPHRyPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgPHRkIGNsYXNzPSJoIj5NaW50ZXIgVmVyc2lvbjwvdGQ+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dGQ+e3sgdmVyc2lvbiB9fTwvdGQ+CiAgICAgICAgICAgICAgICAgICAgICAgIDwvdHI+CiAgICAgICAgICAgICAgICAgICAgICAgIDx0cj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0ZCBjbGFzcz0iaCI+VGVuZGVybWludCBWZXJzaW9uPC90ZD4KICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0ZD57eyBzdGF0dXMubm9kZV9pbmZvLnZlcnNpb24gfX08L3RkPgogICAgICAgICAgICAgICAgICAgICAgICA8L3RyPgogICAgICAgICAgICAgICAgICAgICAgICA8L3Rib2R5PgogICAgICAgICAgICAgICAgICAgIDwvdGFibGU+CiAgICAgICAgICAgICAgICA8L2Rpdj4KICAgICAgICAgICAgICAgIDxkaXYgY2xhc3M9ImNhcmQiIHYtaWY9Im5ldF9pbmZvIj4KICAgICAgICAgICAgICAgICAgICA8ZGl2IGNsYXNzPSJjYXJkLWhlYWRlciI+CiAgICAgICAgICAgICAgICAgICAgICAgIE5ldCBJbmZvCiAgICAgICAgICAgICAgICAgICAgPC9kaXY+CiAgICAgICAgICAgICAgICAgICAgPHRhYmxlIGNsYXNzPSJ0YWJsZSBjYXJkLWJvZHkiPgogICAgICAgICAgICAgICAgICAgICAgICA8dGJvZHk+CiAgICAgICAgICAgICAgICAgICAgICAgIDx0cj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0ZCBjbGFzcz0iaCI+SXMgTGlzdGVuaW5nPC90ZD4KICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0ZD48aSA6Y2xhc3M9InsnZmEtY2hlY2snOiBuZXRfaW5mby5saXN0ZW5pbmd9IiBjbGFzcz0iZmFzIj48L2k+PC90ZD4KICAgICAgICAgICAgICAgICAgICAgICAgPC90cj4KICAgICAgICAgICAgICAgICAgICAgICAgPHRyPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgPHRkIGNsYXNzPSJoIj5Db25uZWN0ZWQgUGVlcnM8L3RkPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgPHRkPnt7IG5ldF9pbmZvLm5fcGVlcnMgfX0gPGkgOmNsYXNzPSJ7J2ZhLWV4Y2xhbWF0aW9uLWNpcmNsZSc6IG5ldF9pbmZvLm5fcGVlcnMgPCAxfSIgY2xhc3M9ImZhcyI+PC9pPjwvdGQ+CiAgICAgICAgICAgICAgICAgICAgICAgIDwvdHI+CiAgICAgICAgICAgICAgICAgICAgICAgIDwvdGJvZHk+CiAgICAgICAgICAgICAgICAgICAgPC90YWJsZT4KICAgICAgICAgICAgICAgIDwvZGl2PgogICAgICAgICAgICA8L2Rpdj4KICAgICAgICAgICAgPGRpdiBjbGFzcz0iY29sIj4KICAgICAgICAgICAgICAgIDxkaXYgY2xhc3M9ImNhcmQiPgogICAgICAgICAgICAgICAgICAgIDxkaXYgY2xhc3M9ImNhcmQtaGVhZGVyIj4KICAgICAgICAgICAgICAgICAgICAgICAgU3luY2luZyBJbmZvCiAgICAgICAgICAgICAgICAgICAgPC9kaXY+CiAgICAgICAgICAgICAgICAgICAgPHRhYmxlIGNsYXNzPSJ0YWJsZSBjYXJkLWJvZHkiPgogICAgICAgICAgICAgICAgICAgICAgICA8dGJvZHk+CiAgICAgICAgICAgICAgICAgICAgICAgIDx0cj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0ZCBjbGFzcz0iaCI+SXMgU3luY2VkPC90ZD4KICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0ZD4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8c3BhbiB2LWlmPSJzdGF0dXMuc3luY19pbmZvLmNhdGNoaW5nX3VwIj5Obzwvc3Bhbj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8c3BhbiB2LWlmPSIhc3RhdHVzLnN5bmNfaW5mby5jYXRjaGluZ191cCI+WWVzPC9zcGFuPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDxpIDpjbGFzcz0ieydmYS1jaGVjayc6ICFzdGF0dXMuc3luY19pbmZvLmNhdGNoaW5nX3VwLCAnZmEtZXhjbGFtYXRpb24tY2lyY2xlJzogc3RhdHVzLnN5bmNfaW5mby5jYXRjaGluZ191cH0iIGNsYXNzPSJmYXMiPjwvaT48L3RkPgogICAgICAgICAgICAgICAgICAgICAgICA8L3RyPgogICAgICAgICAgICAgICAgICAgICAgICA8dHI+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dGQgY2xhc3M9ImgiPkxhdGVzdCBCbG9jayBIZWlnaHQ8L3RkPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgPHRkPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICN7eyBzdGF0dXMuc3luY19pbmZvLmxhdGVzdF9ibG9ja19oZWlnaHQgfX0gPHNwYW4gdi1pZj0ibWFzdGVyU3RhdHVzICYmIE51bWJlcihzdGF0dXMuc3luY19pbmZvLmxhdGVzdF9ibG9ja19oZWlnaHQpIDw9IE51bWJlcihtYXN0ZXJTdGF0dXMubGF0ZXN0X2Jsb2NrX2hlaWdodCkiIGNsYXNzPSJ0ZXh0LW11dGVkIj5vZiB7eyBtYXN0ZXJTdGF0dXMubGF0ZXN0X2Jsb2NrX2hlaWdodCB9fTwvc3Bhbj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvdGQ+CiAgICAgICAgICAgICAgICAgICAgICAgIDwvdHI+CiAgICAgICAgICAgICAgICAgICAgICAgIDx0cj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0ZCBjbGFzcz0iaCI+TGF0ZXN0IEJsb2NrIFRpbWU8L3RkPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgPHRkPgogICAgICAgICAgICAgICAgICAgICAgICAgICAge3sgc3RhdHVzLnN5bmNfaW5mby5sYXRlc3RfYmxvY2tfdGltZSB9fQogICAgICAgICAgICAgICAgICAgICAgICAgICAgPC90ZD4KICAgICAgICAgICAgICAgICAgICAgICAgPC90cj4KICAgICAgICAgICAgICAgICAgICAgICAgPC90Ym9keT4KICAgICAgICAgICAgICAgICAgICA8L3RhYmxlPgogICAgICAgICAgICAgICAgPC9kaXY+CiAgICAgICAgICAgICAgICA8ZGl2IGNsYXNzPSJjYXJkIj4KICAgICAgICAgICAgICAgICAgICA8ZGl2IGNsYXNzPSJjYXJkLWhlYWRlciI+CiAgICAgICAgICAgICAgICAgICAgICAgIFZhbGlkYXRvciBJbmZvCiAgICAgICAgICAgICAgICAgICAgPC9kaXY+CiAgICAgICAgICAgICAgICAgICAgPHRhYmxlIGNsYXNzPSJ0YWJsZSBjYXJkLWJvZHkiPgogICAgICAgICAgICAgICAgICAgICAgICA8dGJvZHk+CiAgICAgICAgICAgICAgICAgICAgICAgIDx0cj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0ZD5QdWJsaWMgS2V5PC90ZD4KICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0ZD57eyB2YWxpZGF0b3JQdWJLZXkgfX08L3RkPgogICAgICAgICAgICAgICAgICAgICAgICA8L3RyPgogICAgICAgICAgICAgICAgICAgICAgICA8dHI+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dGQ+U3RhdHVzPC90ZD4KICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0ZD57eyB2YWxpZGF0b3JTdGF0dXMgfX08L3RkPgogICAgICAgICAgICAgICAgICAgICAgICA8L3RyPgogICAgICAgICAgICAgICAgICAgICAgICA8dHI+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dGQ+VG90YWwgU3Rha2U8L3RkPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgPHRkPnt7IHN0YWtlIH19IE1OVDwvdGQ+CiAgICAgICAgICAgICAgICAgICAgICAgIDwvdHI+CiAgICAgICAgICAgICAgICAgICAgICAgIDx0cj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0ZD5Wb3RpbmcgUG93ZXI8L3RkPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgPHRkPnt7IG5pY2VOdW0oc3RhdHVzLnZhbGlkYXRvcl9pbmZvLnZvdGluZ19wb3dlcikgfX0gPHNwYW4gY2xhc3M9InRleHQtbXV0ZWQiPm9mIDEwMCwwMDAsMDAwPC9zcGFuPjwvdGQ+CiAgICAgICAgICAgICAgICAgICAgICAgIDwvdHI+CiAgICAgICAgICAgICAgICAgICAgICAgIDwvdGJvZHk+CiAgICAgICAgICAgICAgICAgICAgPC90YWJsZT4KICAgICAgICAgICAgICAgIDwvZGl2PgogICAgICAgICAgICA8L2Rpdj4KICAgICAgICA8L2Rpdj4KICAgIDwvZGl2Pgo8L2Rpdj4KPHNjcmlwdD4KICAgIG5ldyBWdWUoewogICAgICAgIGVsOiAnI2FwcCcsCiAgICAgICAgZGF0YTogewogICAgICAgICAgICBtYXN0ZXJTdGF0dXM6IG51bGwsCiAgICAgICAgICAgIHN0YXR1czogbnVsbCwKICAgICAgICAgICAgdmVyc2lvbjogbnVsbCwKICAgICAgICAgICAgbmV0X2luZm86IG51bGwsCiAgICAgICAgICAgIGVycm9yOiBudWxsLAogICAgICAgICAgICB2YWxpZGF0b3JQdWJLZXk6ICcuLi4nLAogICAgICAgICAgICB2YWxpZGF0b3JTdGF0dXM6ICcuLi4nLAogICAgICAgICAgICBzdGFrZTogJy4uLicKICAgICAgICB9LAogICAgICAgIG1vdW50ZWQoKSB7CiAgICAgICAgICAgIHRoaXMucmVmcmVzaCgpCiAgICAgICAgfSwKICAgICAgICBtZXRob2RzOiB7CiAgICAgICAgICAgIG5pY2VOdW0obnVtKSB7CiAgICAgICAgICAgICAgICByZXR1cm4gbnVtLnRvU3RyaW5nKCkucmVwbGFjZSgvXEIoPz0oXGR7M30pKyg/IVxkKSkvZywgIiwiKQogICAgICAgICAgICB9LAogICAgICAgICAgICBiYXNlNjRUb0hleChiYXNlNjQpIHsKICAgICAgICAgICAgICAgIHJldHVybiBDcnlwdG9KUy5lbmMuQmFzZTY0LnBhcnNlKGJhc2U2NCkudG9TdHJpbmcoKQogICAgICAgICAgICB9LAogICAgICAgICAgICByZWZyZXNoKCkgewogICAgICAgICAgICAgICAgYXhpb3MuYWxsKFsKICAgICAgICAgICAgICAgICAgICBheGlvcy5nZXQoIi8vIiArIHdpbmRvdy5sb2NhdGlvbi5ob3N0bmFtZSArICc6ODg0MS9zdGF0dXMnKSwKICAgICAgICAgICAgICAgICAgICBheGlvcy5nZXQoIi8vIiArIHdpbmRvdy5sb2NhdGlvbi5ob3N0bmFtZSArICc6ODg0MS9uZXRfaW5mbycpLAogICAgICAgICAgICAgICAgXSkudGhlbihheGlvcy5zcHJlYWQoZnVuY3Rpb24gKHN0YXR1cywgbmV0X2luZm8pIHsKICAgICAgICAgICAgICAgICAgICB0aGlzLmVycm9yID0gbnVsbAoKICAgICAgICAgICAgICAgICAgICB0aGlzLnN0YXR1cyA9IHN0YXR1cy5kYXRhLnJlc3VsdC50bV9zdGF0dXMKICAgICAgICAgICAgICAgICAgICB0aGlzLnZlcnNpb24gPSBzdGF0dXMuZGF0YS5yZXN1bHQudmVyc2lvbgogICAgICAgICAgICAgICAgICAgIHRoaXMubmV0X2luZm8gPSBuZXRfaW5mby5kYXRhLnJlc3VsdAoKICAgICAgICAgICAgICAgICAgICB0aGlzLnZhbGlkYXRvclB1YktleSA9ICdNcCcgKyB0aGlzLmJhc2U2NFRvSGV4KHN0YXR1cy5kYXRhLnJlc3VsdC50bV9zdGF0dXMudmFsaWRhdG9yX2luZm8ucHViX2tleS52YWx1ZSkKCiAgICAgICAgICAgICAgICAgICAgYXhpb3MuYWxsKFsKICAgICAgICAgICAgICAgICAgICAgICAgYXhpb3MuZ2V0KCIvLyIgKyB3aW5kb3cubG9jYXRpb24uaG9zdG5hbWUgKyAnOjg4NDEvdmFsaWRhdG9ycycpLAogICAgICAgICAgICAgICAgICAgICAgICBheGlvcy5nZXQoIi8vIiArIHdpbmRvdy5sb2NhdGlvbi5ob3N0bmFtZSArICc6ODg0MS9jYW5kaWRhdGU/cHVia2V5PScgKyB0aGlzLnZhbGlkYXRvclB1YktleSksCiAgICAgICAgICAgICAgICAgICAgXSkudGhlbihheGlvcy5zcHJlYWQoZnVuY3Rpb24gKHZhbGlkYXRvcnMsIGNhbmRpZGF0ZSkgewoKICAgICAgICAgICAgICAgICAgICAgICAgdGhpcy5zdGFrZSA9IE1hdGgucm91bmQoY2FuZGlkYXRlLmRhdGEucmVzdWx0LnRvdGFsX3N0YWtlIC8gTWF0aC5wb3coMTAsIDE3KSkgLyAxMAoKICAgICAgICAgICAgICAgICAgICAgICAgaWYgKHZhbGlkYXRvcnMuZGF0YS5yZXN1bHQuZmluZChmdW5jdGlvbih2YWwpIHsgcmV0dXJuIHZhbC5wdWJrZXkgPT09IHRoaXMudmFsaWRhdG9yUHViS2V5IH0uYmluZCh0aGlzKSkpIHsKICAgICAgICAgICAgICAgICAgICAgICAgICAgIHRoaXMudmFsaWRhdG9yU3RhdHVzID0gJ1ZhbGlkYXRpbmcnOwogICAgICAgICAgICAgICAgICAgICAgICAgICAgcmV0dXJuCiAgICAgICAgICAgICAgICAgICAgICAgIH0KCiAgICAgICAgICAgICAgICAgICAgICAgIGlmIChjYW5kaWRhdGUuZGF0YS5yZXN1bHQuc3RhdHVzID09PSAyKSB7CiAgICAgICAgICAgICAgICAgICAgICAgICAgICB0aGlzLnZhbGlkYXRvclN0YXR1cyA9ICdDYW5kaWRhdGUnCiAgICAgICAgICAgICAgICAgICAgICAgICAgICByZXR1cm4KICAgICAgICAgICAgICAgICAgICAgICAgfQoKICAgICAgICAgICAgICAgICAgICAgICAgdGhpcy52YWxpZGF0b3JTdGF0dXMgPSAnRG93bic7CiAgICAgICAgICAgICAgICAgICAgfS5iaW5kKHRoaXMpKSkuY2F0Y2goZnVuY3Rpb24oKSAgewogICAgICAgICAgICAgICAgICAgICAgICB0aGlzLnZhbGlkYXRvclN0YXR1cyA9ICdOb3QgZGVjbGFyZWQnOwogICAgICAgICAgICAgICAgICAgICAgICB0aGlzLnN0YWtlID0gMDsKICAgICAgICAgICAgICAgICAgICB9LmJpbmQodGhpcykpOwoKICAgICAgICAgICAgICAgICAgICBzZXRUaW1lb3V0KHRoaXMucmVmcmVzaCwgNTAwMCkKICAgICAgICAgICAgICAgIH0uYmluZCh0aGlzKSkpLmNhdGNoKGZ1bmN0aW9uIChyZWFzb24pIHsKICAgICAgICAgICAgICAgICAgICB0aGlzLmVycm9yID0gcmVhc29uLnRvU3RyaW5nKCk7CiAgICAgICAgICAgICAgICAgICAgc2V0VGltZW91dCh0aGlzLnJlZnJlc2gsIDUwMDApCiAgICAgICAgICAgICAgICB9LmJpbmQodGhpcykpCgogICAgICAgICAgICAgICAgYXhpb3MuZ2V0KCJodHRwczovL21pbnRlci1ub2RlLTEudGVzdG5ldC5taW50ZXIubmV0d29yay9zdGF0dXMiKS50aGVuKGZ1bmN0aW9uIChtYXN0ZXJTdGF0dXMpIHsKICAgICAgICAgICAgICAgICAgICB0aGlzLm1hc3RlclN0YXR1cyA9IG1hc3RlclN0YXR1cy5kYXRhLnJlc3VsdAogICAgICAgICAgICAgICAgfS5iaW5kKHRoaXMpKQogICAgICAgICAgICB9CiAgICAgICAgfQogICAgfSkKPC9zY3JpcHQ+CjwvYm9keT4KPC9odG1sPg==\"") } diff --git a/gui/html/index.html b/gui/html/index.html index cf683010e..fba2a31bc 100644 --- a/gui/html/index.html +++ b/gui/html/index.html @@ -229,7 +229,7 @@

Error while connecting to local node

this.stake = Math.round(candidate.data.result.total_stake / Math.pow(10, 17)) / 10 - if (validators.data.result.find(function(val) { return val.candidate.pub_key === this.validatorPubKey }.bind(this))) { + if (validators.data.result.find(function(val) { return val.pubkey === this.validatorPubKey }.bind(this))) { this.validatorStatus = 'Validating'; return } From b6e4d6c1d396043117acf57caf32f744c0b03ce9 Mon Sep 17 00:00:00 2001 From: Daniil Lashin Date: Thu, 6 Dec 2018 11:55:19 +0300 Subject: [PATCH 47/49] Fix get_tools command --- Makefile | 9 +++++---- scripts/get_tools.sh | 43 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 4 deletions(-) create mode 100755 scripts/get_tools.sh diff --git a/Makefile b/Makefile index db9b84276..3902336aa 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,8 @@ GOTOOLS = \ - github.com/golang/dep/cmd/dep \ - gopkg.in/alecthomas/gometalinter.v2 \ + github.com/mitchellh/gox \ + github.com/golang/dep/cmd/dep \ + github.com/alecthomas/gometalinter \ + github.com/gogo/protobuf/protoc-gen-gogo \ github.com/gobuffalo/packr/packr PACKAGES=$(shell go list ./... | grep -v '/vendor/') BUILD_TAGS?=minter @@ -30,8 +32,7 @@ check_tools: get_tools: @echo "--> Installing tools" - go get -u -v $(GOTOOLS) - @gometalinter.v2 --install + ./scripts/get_tools.sh update_tools: @echo "--> Updating tools" diff --git a/scripts/get_tools.sh b/scripts/get_tools.sh new file mode 100755 index 000000000..3a6839cd3 --- /dev/null +++ b/scripts/get_tools.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash +set -e + +## check if GOPATH is set +if [ -z ${GOPATH+x} ]; then + echo "please set GOPATH (https://github.com/golang/go/wiki/SettingGOPATH)" + exit 1 +fi + +mkdir -p "$GOPATH/src/github.com" +cd "$GOPATH/src/github.com" || exit 1 + +installFromGithub() { + repo=$1 + commit=$2 + # optional + subdir=$3 + echo "--> Installing $repo ($commit)..." + if [ ! -d "$repo" ]; then + mkdir -p "$repo" + git clone "https://github.com/$repo.git" "$repo" + fi + if [ ! -z ${subdir+x} ] && [ ! -d "$repo/$subdir" ]; then + echo "ERROR: no such directory $repo/$subdir" + exit 1 + fi + pushd "$repo" && \ + git fetch origin && \ + git checkout -q "$commit" && \ + if [ ! -z ${subdir+x} ]; then cd "$subdir" || exit 1; fi && \ + go install && \ + if [ ! -z ${subdir+x} ]; then cd - || exit 1; fi && \ + popd || exit 1 + echo "--> Done" + echo "" +} + +installFromGithub mitchellh/gox 51ed453898ca5579fea9ad1f08dff6b121d9f2e8 +installFromGithub golang/dep 22125cfaa6ddc71e145b1535d4b7ee9744fefff2 cmd/dep +installFromGithub alecthomas/gometalinter 17a7ffa42374937bfecabfb8d2efbd4db0c26741 +installFromGithub gogo/protobuf 61dbc136cf5d2f08d68a011382652244990a53a9 protoc-gen-gogo +installFromGithub square/certstrap e27060a3643e814151e65b9807b6b06d169580a7 +installFromGithub gobuffalo/packr 0bf68f085c6ef75e9f2403052b9e79ddd42a660e From 8ef24d2a17006f554b27a4d795bc5ea991acef0e Mon Sep 17 00:00:00 2001 From: Daniil Lashin Date: Thu, 6 Dec 2018 11:57:00 +0300 Subject: [PATCH 48/49] Restrict compilation rules --- scripts/dist.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/dist.sh b/scripts/dist.sh index e18ec723f..046446f50 100644 --- a/scripts/dist.sh +++ b/scripts/dist.sh @@ -24,8 +24,8 @@ GIT_COMMIT="$(git rev-parse --short=8 HEAD)" GIT_IMPORT="github.com/MinterTeam/minter-go-node/version" # Determine the arch/os combos we're building for -XC_ARCH=${XC_ARCH:-"386 amd64 arm"} -XC_OS=${XC_OS:-"solaris darwin freebsd linux windows"} +XC_ARCH=${XC_ARCH:-"amd64"} +XC_OS=${XC_OS:-"darwin linux windows"} XC_EXCLUDE=${XC_EXCLUDE:-" darwin/arm solaris/amd64 solaris/386 solaris/arm freebsd/amd64 windows/arm "} # Make sure build tools are available. From 4f3b9399e359dd357230ed2a93643b82650ad948 Mon Sep 17 00:00:00 2001 From: Daniil Lashin Date: Thu, 6 Dec 2018 15:30:32 +0300 Subject: [PATCH 49/49] Add seed and remove persistent peer in config --- config/config.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/config.go b/config/config.go index 1df5d74cc..1b3c5029a 100644 --- a/config/config.go +++ b/config/config.go @@ -51,7 +51,7 @@ func init() { func DefaultConfig() *Config { cfg := defaultConfig() - cfg.P2P.PersistentPeers = "647e32df3b9c54809b5aca2877d9ba60900bc2d9@minter-node-1.testnet.minter.network:26656" + cfg.P2P.Seeds = "d20522aa7ba4af8139749c5e724063c4ba18c58b@minter-node-2.testnet.minter.network:26656" cfg.TxIndex = &tmConfig.TxIndexConfig{ Indexer: "kv",