Merge branch 'master' into develop
Conflicts: cmd/tendermint/flags.go glide.lock glide.yaml node/node.go rpc/core/routes.go version/version.go
This commit is contained in:
commit
f347143b3d
45
README.md
45
README.md
|
@ -1,5 +1,8 @@
|
||||||
# Tendermint
|
# Tendermint
|
||||||
Simple, Secure, Scalable Blockchain Platform
|
|
||||||
|
[Byzantine-Fault Tolerant](https://en.wikipedia.org/wiki/Byzantine_fault_tolerance)
|
||||||
|
[State Machine Replication](https://en.wikipedia.org/wiki/State_machine_replication).
|
||||||
|
Or [Blockchain](https://en.wikipedia.org/wiki/Blockchain_(database)) for short.
|
||||||
|
|
||||||
[![version](https://img.shields.io/github/tag/tendermint/tendermint.svg)](https://github.com/tendermint/tendermint/releases/latest)
|
[![version](https://img.shields.io/github/tag/tendermint/tendermint.svg)](https://github.com/tendermint/tendermint/releases/latest)
|
||||||
[![API Reference](
|
[![API Reference](
|
||||||
|
@ -16,26 +19,29 @@ master | [![CircleCI](https://circleci.com/gh/tendermint/tendermint/tree/mast
|
||||||
_NOTE: This is yet pre-alpha non-production-quality software._
|
_NOTE: This is yet pre-alpha non-production-quality software._
|
||||||
|
|
||||||
Tendermint Core is Byzantine Fault Tolerant (BFT) middleware that takes a state transition machine, written in any programming language,
|
Tendermint Core is Byzantine Fault Tolerant (BFT) middleware that takes a state transition machine, written in any programming language,
|
||||||
and replicates it on many machines.
|
and securely replicates it on many machines.
|
||||||
See the [application developers guide](https://github.com/tendermint/tendermint/wiki/Application-Developers) to get started.
|
|
||||||
|
For more background, see the [introduction](https://tendermint.com/intro).
|
||||||
|
|
||||||
|
To get started developing applications, see the [application developers guide](https://tendermint.com/docs/guides/app-development).
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
`go get -u github.com/tendermint/tendermint/cmd/tendermint`
|
||||||
|
|
||||||
|
For more details (or if it fails), see the [install guide](https://tendermint.com/intro/getting-started/install).
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
Yay open source! Please see our [contributing guidelines](https://github.com/tendermint/tendermint/wiki/Contributing).
|
Yay open source! Please see our [contributing guidelines](https://tendermint.com/guides/contributing).
|
||||||
|
|
||||||
## Resources
|
## Resources
|
||||||
|
|
||||||
### Tendermint Core
|
### Tendermint Core
|
||||||
|
|
||||||
- [Introduction](https://github.com/tendermint/tendermint/wiki/Introduction)
|
- [Introduction](https://tendermint.com/intro)
|
||||||
- [Validators](https://github.com/tendermint/tendermint/wiki/Validators)
|
- [Docs](https://tendermint.com/docs)
|
||||||
- [Byzantine Consensus Algorithm](https://github.com/tendermint/tendermint/wiki/Byzantine-Consensus-Algorithm)
|
- [Software using Tendermint](https://tendermint.com/ecosystem)
|
||||||
- [Block Structure](https://github.com/tendermint/tendermint/wiki/Block-Structure)
|
|
||||||
- [RPC](https://github.com/tendermint/tendermint/wiki/RPC)
|
|
||||||
- [Genesis](https://github.com/tendermint/tendermint/wiki/Genesis)
|
|
||||||
- [Configuration](https://github.com/tendermint/tendermint/wiki/Configuration)
|
|
||||||
- [Light Client Protocol](https://github.com/tendermint/tendermint/wiki/Light-Client-Protocol)
|
|
||||||
- [Roadmap for V2](https://github.com/tendermint/tendermint/wiki/Roadmap-for-V2)
|
|
||||||
|
|
||||||
### Sub-projects
|
### Sub-projects
|
||||||
|
|
||||||
|
@ -45,8 +51,15 @@ Yay open source! Please see our [contributing guidelines](https://github.com/ten
|
||||||
* [Go-P2P](http://github.com/tendermint/go-p2p)
|
* [Go-P2P](http://github.com/tendermint/go-p2p)
|
||||||
* [Go-Merkle](http://github.com/tendermint/go-merkle)
|
* [Go-Merkle](http://github.com/tendermint/go-merkle)
|
||||||
|
|
||||||
## Install
|
### Applications
|
||||||
|
|
||||||
`go get -u github.com/tendermint/tendermint/cmd/tendermint`
|
* [Ethermint](http://github.com/tendermint/ethermint)
|
||||||
|
* [Basecoin](http://github.com/tendermint/basecoin)
|
||||||
|
|
||||||
|
### More
|
||||||
|
|
||||||
|
* [Tendermint Blog](https://tendermint.com/blog)
|
||||||
|
* [Cosmos Blog](https://cosmos.network/blog)
|
||||||
|
* [Original Whitepaper (out-of-date)](http://www.the-blockchain.com/docs/Tendermint%20Consensus%20without%20Mining.pdf)
|
||||||
|
* [Master's Thesis on Tendermint](https://atrium.lib.uoguelph.ca/xmlui/handle/10214/9769)
|
||||||
|
|
||||||
For more details, see the [install guide](https://github.com/tendermint/tendermint/wiki/Installation).
|
|
||||||
|
|
|
@ -20,11 +20,15 @@ func parseFlags(config cfg.Config, args []string) {
|
||||||
logLevel string
|
logLevel string
|
||||||
proxyApp string
|
proxyApp string
|
||||||
abciTransport string
|
abciTransport string
|
||||||
|
|
||||||
|
pex bool
|
||||||
)
|
)
|
||||||
|
|
||||||
// Declare flags
|
// Declare flags
|
||||||
var flags = flag.NewFlagSet("main", flag.ExitOnError)
|
var flags = flag.NewFlagSet("main", flag.ExitOnError)
|
||||||
flags.BoolVar(&printHelp, "help", false, "Print this help message.")
|
flags.BoolVar(&printHelp, "help", false, "Print this help message.")
|
||||||
|
|
||||||
|
// configuration options
|
||||||
flags.StringVar(&moniker, "moniker", config.GetString("moniker"), "Node Name")
|
flags.StringVar(&moniker, "moniker", config.GetString("moniker"), "Node Name")
|
||||||
flags.StringVar(&nodeLaddr, "node_laddr", config.GetString("node_laddr"), "Node listen address. (0.0.0.0:0 means any interface, any port)")
|
flags.StringVar(&nodeLaddr, "node_laddr", config.GetString("node_laddr"), "Node listen address. (0.0.0.0:0 means any interface, any port)")
|
||||||
flags.StringVar(&seeds, "seeds", config.GetString("seeds"), "Comma delimited host:port seed nodes")
|
flags.StringVar(&seeds, "seeds", config.GetString("seeds"), "Comma delimited host:port seed nodes")
|
||||||
|
@ -36,6 +40,10 @@ func parseFlags(config cfg.Config, args []string) {
|
||||||
flags.StringVar(&proxyApp, "proxy_app", config.GetString("proxy_app"),
|
flags.StringVar(&proxyApp, "proxy_app", config.GetString("proxy_app"),
|
||||||
"Proxy app address, or 'nilapp' or 'dummy' for local testing.")
|
"Proxy app address, or 'nilapp' or 'dummy' for local testing.")
|
||||||
flags.StringVar(&abciTransport, "abci", config.GetString("abci"), "Specify abci transport (socket | grpc)")
|
flags.StringVar(&abciTransport, "abci", config.GetString("abci"), "Specify abci transport (socket | grpc)")
|
||||||
|
|
||||||
|
// feature flags
|
||||||
|
flags.BoolVar(&pex, "pex", config.GetBool("pex_reactor"), "Enable Peer-Exchange (dev feature)")
|
||||||
|
|
||||||
flags.Parse(args)
|
flags.Parse(args)
|
||||||
if printHelp {
|
if printHelp {
|
||||||
flags.PrintDefaults()
|
flags.PrintDefaults()
|
||||||
|
@ -53,4 +61,6 @@ func parseFlags(config cfg.Config, args []string) {
|
||||||
config.Set("log_level", logLevel)
|
config.Set("log_level", logLevel)
|
||||||
config.Set("proxy_app", proxyApp)
|
config.Set("proxy_app", proxyApp)
|
||||||
config.Set("abci", abciTransport)
|
config.Set("abci", abciTransport)
|
||||||
|
|
||||||
|
config.Set("pex_reactor", pex)
|
||||||
}
|
}
|
||||||
|
|
|
@ -62,6 +62,8 @@ func GetConfig(rootDir string) cfg.Config {
|
||||||
mapConfig.SetDefault("fast_sync", true)
|
mapConfig.SetDefault("fast_sync", true)
|
||||||
mapConfig.SetDefault("skip_upnp", false)
|
mapConfig.SetDefault("skip_upnp", false)
|
||||||
mapConfig.SetDefault("addrbook_file", rootDir+"/addrbook.json")
|
mapConfig.SetDefault("addrbook_file", rootDir+"/addrbook.json")
|
||||||
|
mapConfig.SetDefault("addrbook_strict", true) // disable to allow connections locally
|
||||||
|
mapConfig.SetDefault("pex_reactor", false) // enable for peer exchange
|
||||||
mapConfig.SetDefault("priv_validator_file", rootDir+"/priv_validator.json")
|
mapConfig.SetDefault("priv_validator_file", rootDir+"/priv_validator.json")
|
||||||
mapConfig.SetDefault("db_backend", "leveldb")
|
mapConfig.SetDefault("db_backend", "leveldb")
|
||||||
mapConfig.SetDefault("db_dir", rootDir+"/data")
|
mapConfig.SetDefault("db_dir", rootDir+"/data")
|
||||||
|
|
|
@ -76,6 +76,8 @@ func ResetConfig(localPath string) cfg.Config {
|
||||||
mapConfig.SetDefault("fast_sync", false)
|
mapConfig.SetDefault("fast_sync", false)
|
||||||
mapConfig.SetDefault("skip_upnp", true)
|
mapConfig.SetDefault("skip_upnp", true)
|
||||||
mapConfig.SetDefault("addrbook_file", rootDir+"/addrbook.json")
|
mapConfig.SetDefault("addrbook_file", rootDir+"/addrbook.json")
|
||||||
|
mapConfig.SetDefault("addrbook_strict", true) // disable to allow connections locally
|
||||||
|
mapConfig.SetDefault("pex_reactor", false) // enable for peer exchange
|
||||||
mapConfig.SetDefault("priv_validator_file", rootDir+"/priv_validator.json")
|
mapConfig.SetDefault("priv_validator_file", rootDir+"/priv_validator.json")
|
||||||
mapConfig.SetDefault("db_backend", "memdb")
|
mapConfig.SetDefault("db_backend", "memdb")
|
||||||
mapConfig.SetDefault("db_dir", rootDir+"/data")
|
mapConfig.SetDefault("db_dir", rootDir+"/data")
|
||||||
|
|
|
@ -62,6 +62,8 @@ imports:
|
||||||
- extra25519
|
- extra25519
|
||||||
- name: github.com/tendermint/go-autofile
|
- name: github.com/tendermint/go-autofile
|
||||||
version: 0416e0aa9c68205aa44844096f9f151ada9d0405
|
version: 0416e0aa9c68205aa44844096f9f151ada9d0405
|
||||||
|
- name: github.com/tendermint/go-flowrate
|
||||||
|
version: a20c98e61957faa93b4014fbd902f20ab9317a6a
|
||||||
- name: github.com/tendermint/go-clist
|
- name: github.com/tendermint/go-clist
|
||||||
version: 3baa390bbaf7634251c42ad69a8682e7e3990552
|
version: 3baa390bbaf7634251c42ad69a8682e7e3990552
|
||||||
- name: github.com/tendermint/go-common
|
- name: github.com/tendermint/go-common
|
||||||
|
|
|
@ -33,3 +33,6 @@ import:
|
||||||
- package: golang.org/x/crypto
|
- package: golang.org/x/crypto
|
||||||
subpackages:
|
subpackages:
|
||||||
- ripemd160
|
- ripemd160
|
||||||
|
- package: github.com/tendermint/go-flowrate
|
||||||
|
subpackages:
|
||||||
|
- flowrate
|
||||||
|
|
|
@ -113,6 +113,15 @@ func NewNode(config cfg.Config, privValidator *types.PrivValidator, clientCreato
|
||||||
sw.AddReactor("BLOCKCHAIN", bcReactor)
|
sw.AddReactor("BLOCKCHAIN", bcReactor)
|
||||||
sw.AddReactor("CONSENSUS", consensusReactor)
|
sw.AddReactor("CONSENSUS", consensusReactor)
|
||||||
|
|
||||||
|
// Optionally, start the pex reactor
|
||||||
|
// TODO: this is a dev feature, it needs some love
|
||||||
|
if config.GetBool("pex_reactor") {
|
||||||
|
addrBook := p2p.NewAddrBook(config.GetString("addrbook_file"), config.GetBool("addrbook_strict"))
|
||||||
|
addrBook.Start()
|
||||||
|
pexReactor := p2p.NewPEXReactor(addrBook)
|
||||||
|
sw.AddReactor("PEX", pexReactor)
|
||||||
|
}
|
||||||
|
|
||||||
// filter peers by addr or pubkey with a abci query.
|
// filter peers by addr or pubkey with a abci query.
|
||||||
// if the query return code is OK, add peer
|
// if the query return code is OK, add peer
|
||||||
// XXX: query format subject to change
|
// XXX: query format subject to change
|
||||||
|
|
|
@ -9,6 +9,7 @@ import (
|
||||||
|
|
||||||
//-----------------------------------------------------------------------------
|
//-----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// TODO: limit/permission on (max - min)
|
||||||
func BlockchainInfo(minHeight, maxHeight int) (*ctypes.ResultBlockchainInfo, error) {
|
func BlockchainInfo(minHeight, maxHeight int) (*ctypes.ResultBlockchainInfo, error) {
|
||||||
if maxHeight == 0 {
|
if maxHeight == 0 {
|
||||||
maxHeight = blockStore.Height()
|
maxHeight = blockStore.Height()
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
package core
|
package core
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
|
|
||||||
ctypes "github.com/tendermint/tendermint/rpc/core/types"
|
ctypes "github.com/tendermint/tendermint/rpc/core/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -31,12 +29,8 @@ func NetInfo() (*ctypes.ResultNetInfo, error) {
|
||||||
|
|
||||||
//-----------------------------------------------------------------------------
|
//-----------------------------------------------------------------------------
|
||||||
|
|
||||||
// Dial given list of seeds if we have no outbound peers
|
// Dial given list of seeds
|
||||||
func DialSeeds(seeds []string) (*ctypes.ResultDialSeeds, error) {
|
func UnsafeDialSeeds(seeds []string) (*ctypes.ResultDialSeeds, error) {
|
||||||
outbound, _, _ := p2pSwitch.NumPeers()
|
|
||||||
if outbound != 0 {
|
|
||||||
return nil, fmt.Errorf("Already have some outbound peers")
|
|
||||||
}
|
|
||||||
// starts go routines to dial each seed after random delays
|
// starts go routines to dial each seed after random delays
|
||||||
p2pSwitch.DialSeeds(seeds)
|
p2pSwitch.DialSeeds(seeds)
|
||||||
return &ctypes.ResultDialSeeds{}, nil
|
return &ctypes.ResultDialSeeds{}, nil
|
||||||
|
|
|
@ -6,30 +6,38 @@ import (
|
||||||
ctypes "github.com/tendermint/tendermint/rpc/core/types"
|
ctypes "github.com/tendermint/tendermint/rpc/core/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// TODO: better system than "unsafe" prefix
|
||||||
var Routes = map[string]*rpc.RPCFunc{
|
var Routes = map[string]*rpc.RPCFunc{
|
||||||
// subscribe/unsubscribe are reserved for websocket events.
|
// subscribe/unsubscribe are reserved for websocket events.
|
||||||
"subscribe": rpc.NewWSRPCFunc(SubscribeResult, "event"),
|
"subscribe": rpc.NewWSRPCFunc(SubscribeResult, "event"),
|
||||||
"unsubscribe": rpc.NewWSRPCFunc(UnsubscribeResult, "event"),
|
"unsubscribe": rpc.NewWSRPCFunc(UnsubscribeResult, "event"),
|
||||||
|
|
||||||
|
// info API
|
||||||
"status": rpc.NewRPCFunc(StatusResult, ""),
|
"status": rpc.NewRPCFunc(StatusResult, ""),
|
||||||
"net_info": rpc.NewRPCFunc(NetInfoResult, ""),
|
"net_info": rpc.NewRPCFunc(NetInfoResult, ""),
|
||||||
"dial_seeds": rpc.NewRPCFunc(DialSeedsResult, "seeds"),
|
|
||||||
"blockchain": rpc.NewRPCFunc(BlockchainInfoResult, "minHeight,maxHeight"),
|
"blockchain": rpc.NewRPCFunc(BlockchainInfoResult, "minHeight,maxHeight"),
|
||||||
"genesis": rpc.NewRPCFunc(GenesisResult, ""),
|
"genesis": rpc.NewRPCFunc(GenesisResult, ""),
|
||||||
"block": rpc.NewRPCFunc(BlockResult, "height"),
|
"block": rpc.NewRPCFunc(BlockResult, "height"),
|
||||||
"validators": rpc.NewRPCFunc(ValidatorsResult, ""),
|
"validators": rpc.NewRPCFunc(ValidatorsResult, ""),
|
||||||
"dump_consensus_state": rpc.NewRPCFunc(DumpConsensusStateResult, ""),
|
"dump_consensus_state": rpc.NewRPCFunc(DumpConsensusStateResult, ""),
|
||||||
"broadcast_tx_commit": rpc.NewRPCFunc(BroadcastTxCommitResult, "tx"),
|
|
||||||
"broadcast_tx_sync": rpc.NewRPCFunc(BroadcastTxSyncResult, "tx"),
|
|
||||||
"broadcast_tx_async": rpc.NewRPCFunc(BroadcastTxAsyncResult, "tx"),
|
|
||||||
"unconfirmed_txs": rpc.NewRPCFunc(UnconfirmedTxsResult, ""),
|
"unconfirmed_txs": rpc.NewRPCFunc(UnconfirmedTxsResult, ""),
|
||||||
"num_unconfirmed_txs": rpc.NewRPCFunc(NumUnconfirmedTxsResult, ""),
|
"num_unconfirmed_txs": rpc.NewRPCFunc(NumUnconfirmedTxsResult, ""),
|
||||||
|
|
||||||
|
// broadcast API
|
||||||
|
"broadcast_tx_commit": rpc.NewRPCFunc(BroadcastTxCommitResult, "tx"),
|
||||||
|
"broadcast_tx_sync": rpc.NewRPCFunc(BroadcastTxSyncResult, "tx"),
|
||||||
|
"broadcast_tx_async": rpc.NewRPCFunc(BroadcastTxAsyncResult, "tx"),
|
||||||
|
|
||||||
|
// abci API
|
||||||
"abci_query": rpc.NewRPCFunc(ABCIQueryResult, "query"),
|
"abci_query": rpc.NewRPCFunc(ABCIQueryResult, "query"),
|
||||||
"abci_info": rpc.NewRPCFunc(ABCIInfoResult, ""),
|
"abci_info": rpc.NewRPCFunc(ABCIInfoResult, ""),
|
||||||
|
|
||||||
"unsafe_flush_mempool": rpc.NewRPCFunc(UnsafeFlushMempool, ""),
|
// control API
|
||||||
"unsafe_set_config": rpc.NewRPCFunc(UnsafeSetConfigResult, "type,key,value"),
|
"dial_seeds": rpc.NewRPCFunc(UnsafeDialSeedsResult, "seeds"),
|
||||||
|
"unsafe_flush_mempool": rpc.NewRPCFunc(UnsafeFlushMempool, ""),
|
||||||
|
"unsafe_set_config": rpc.NewRPCFunc(UnsafeSetConfigResult, "type,key,value"),
|
||||||
|
|
||||||
|
// profiler API
|
||||||
"unsafe_start_cpu_profiler": rpc.NewRPCFunc(UnsafeStartCPUProfilerResult, "filename"),
|
"unsafe_start_cpu_profiler": rpc.NewRPCFunc(UnsafeStartCPUProfilerResult, "filename"),
|
||||||
"unsafe_stop_cpu_profiler": rpc.NewRPCFunc(UnsafeStopCPUProfilerResult, ""),
|
"unsafe_stop_cpu_profiler": rpc.NewRPCFunc(UnsafeStopCPUProfilerResult, ""),
|
||||||
"unsafe_write_heap_profile": rpc.NewRPCFunc(UnsafeWriteHeapProfileResult, "filename"),
|
"unsafe_write_heap_profile": rpc.NewRPCFunc(UnsafeWriteHeapProfileResult, "filename"),
|
||||||
|
@ -67,8 +75,8 @@ func NetInfoResult() (ctypes.TMResult, error) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func DialSeedsResult(seeds []string) (ctypes.TMResult, error) {
|
func UnsafeDialSeedsResult(seeds []string) (ctypes.TMResult, error) {
|
||||||
if r, err := DialSeeds(seeds); err != nil {
|
if r, err := UnsafeDialSeeds(seeds); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
} else {
|
} else {
|
||||||
return r, nil
|
return r, nil
|
||||||
|
|
|
@ -1,7 +1,4 @@
|
||||||
#! /bin/bash
|
#! /bin/bash
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
LIB=$1
|
|
||||||
|
|
||||||
set +u
|
set +u
|
||||||
if [[ "$GLIDE" == "" ]]; then
|
if [[ "$GLIDE" == "" ]]; then
|
||||||
|
@ -9,4 +6,8 @@ if [[ "$GLIDE" == "" ]]; then
|
||||||
fi
|
fi
|
||||||
set -u
|
set -u
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
LIB=$1
|
||||||
|
|
||||||
cat $GLIDE | grep -A1 $LIB | grep -v $LIB | awk '{print $2}'
|
cat $GLIDE | grep -A1 $LIB | grep -v $LIB | awk '{print $2}'
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
#! /bin/bash
|
#! /bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
# set glide.lock path
|
# set glide.lock path
|
||||||
if [[ "$GLIDE" == "" ]]; then
|
if [[ "$GLIDE" == "" ]]; then
|
||||||
|
@ -28,10 +29,11 @@ for lib in "${LIBS_GO_TEST[@]}"; do
|
||||||
done
|
done
|
||||||
|
|
||||||
for lib in "${LIBS_MAKE_TEST[@]}"; do
|
for lib in "${LIBS_MAKE_TEST[@]}"; do
|
||||||
getDep $lib
|
|
||||||
|
# checkout vendored version of lib
|
||||||
|
bash scripts/glide/checkout.sh $GLIDE $lib
|
||||||
|
|
||||||
echo "Testing $lib ..."
|
echo "Testing $lib ..."
|
||||||
cd $GOPATH/src/github.com/tendermint/$lib
|
|
||||||
make test
|
make test
|
||||||
if [[ "$?" != 0 ]]; then
|
if [[ "$?" != 0 ]]; then
|
||||||
echo "FAIL"
|
echo "FAIL"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
package version
|
package version
|
||||||
|
|
||||||
const Maj = "0"
|
const Maj = "0"
|
||||||
const Min = "7" // abci useability (protobuf, unix); optimizations; broadcast_tx_commit
|
const Min = "7" // tmsp useability (protobuf, unix); optimizations; broadcast_tx_commit
|
||||||
const Fix = "3" // fixes to event safety, mempool deadlock, hvs race, replay non-empty blocks
|
const Fix = "4" // --pex flag and less restricted /dial_seeds
|
||||||
|
|
||||||
const Version = Maj + "." + Min + "." + Fix
|
const Version = Maj + "." + Min + "." + Fix
|
||||||
|
|
Loading…
Reference in New Issue