Skip to content

Commit 6566015

Browse files
author
Michał Mrożek
authored
Merge pull request #746 from input-output-hk/etcm-202-mantis-cli
[ETCM-202] Provide Mantis command-line interface
2 parents 30b2764 + 8f5e412 commit 6566015

File tree

8 files changed

+225
-2
lines changed

8 files changed

+225
-2
lines changed

README.md

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,23 @@ Example:
2929

3030
Possible networks: `etc`, `eth`, `mordor`, `testnet-internal`
3131

32+
### Command Line Interface
33+
34+
`cli` is a tool that can be used to:
35+
36+
- generate a new private key
37+
```
38+
./bin/mantis cli generate-private-key
39+
```
40+
- derive an address from private key
41+
```
42+
./bin/mantis cli derive-address 00b11c32957057651d56cd83085ef3b259319057e0e887bd0fdaee657e6f75d0
43+
```
44+
- generate genesis allocs (using private keys and/or addresses)
45+
```
46+
`./bin/mantis cli generate-alloc --balance=42 --address=8b196738d90cf3d9fc299e0ec28e15ebdcbb0bdcb281d9d5084182c9c66d5d12 --key=00b11c32957057651d56cd83085ef3b259319057e0e887bd0fdaee657e6f75d1`
47+
```
48+
3249
### Building the client
3350

3451
#### SBT
@@ -86,7 +103,7 @@ This patch will update the lock files for you.
86103

87104
- `repo.nix` : generated by the `sbtix-gen` command and includes only the build dependencies for the project.
88105
- `project/repo.nix` : generated by the `sbtix-gen-all` command and includes only the plugin dependencies. Also generates `repo.nix`.
89-
- `project/project/repo.nix` : generated by the `sbtix-gen-all2` command and includes only the pluginplugin dependencies. Also generates `repo.nix` and `project/repo.nix`.
106+
- `project/project/repo.nix` : generated by the `sbtix-gen-all2` command and includes only the plugin dependencies. Also generates `repo.nix` and `project/repo.nix`.
90107

91108
##### error: unsupported argument 'submodules' to 'fetchGit'
92109

build.sbt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ val dep = {
4242
Dependencies.apacheCommons,
4343
Dependencies.micrometer,
4444
Dependencies.prometheus,
45+
Dependencies.cli,
4546
Dependencies.dependencies
4647
).flatten ++ malletDeps
4748
}

project/Dependencies.scala

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,8 @@ object Dependencies {
8383

8484
val scopt = Seq("com.github.scopt" % "scopt_2.12" % "3.7.1")
8585

86+
val cli = Seq("com.monovore" %% "decline" % "1.3.0")
87+
8688
val apacheCommons = Seq(
8789
"commons-io" % "commons-io" % "2.8.0"
8890
)

repo.nix

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -784,6 +784,22 @@
784784
url = "https://repo1.maven.org/maven2/com/miguno/akka/akka-mock-scheduler_2.12/0.5.5/akka-mock-scheduler_2.12-0.5.5.pom";
785785
sha256 = "00641F962CB8D1BED3C64FFA635CB0562DFD6D52A9B5DD614536EAC864CBD948";
786786
};
787+
"nix-public/com/monovore/decline_2.12/1.3.0/decline_2.12-1.3.0-javadoc.jar" = {
788+
url = "https://repo1.maven.org/maven2/com/monovore/decline_2.12/1.3.0/decline_2.12-1.3.0-javadoc.jar";
789+
sha256 = "9D0962E8AAB7D02EB20B50C428C9D7044B6886E8436FABFC2571DE22E5BD909E";
790+
};
791+
"nix-public/com/monovore/decline_2.12/1.3.0/decline_2.12-1.3.0-sources.jar" = {
792+
url = "https://repo1.maven.org/maven2/com/monovore/decline_2.12/1.3.0/decline_2.12-1.3.0-sources.jar";
793+
sha256 = "1554EA9939430010D27F9EAF22BFA770402F14C33CFAEF6DB52FCD79FA176EA2";
794+
};
795+
"nix-public/com/monovore/decline_2.12/1.3.0/decline_2.12-1.3.0.jar" = {
796+
url = "https://repo1.maven.org/maven2/com/monovore/decline_2.12/1.3.0/decline_2.12-1.3.0.jar";
797+
sha256 = "F2854BF74AE59AB7E19745CA312181DF3EFA2A1A3DA796E8FEC41E2B9C870FB4";
798+
};
799+
"nix-public/com/monovore/decline_2.12/1.3.0/decline_2.12-1.3.0.pom" = {
800+
url = "https://repo1.maven.org/maven2/com/monovore/decline_2.12/1.3.0/decline_2.12-1.3.0.pom";
801+
sha256 = "8806217334913555CE9E8F844EA4561767CD850457945E552838F1B2F85A9C86";
802+
};
787803
"nix-public/com/squareup/okhttp3/logging-interceptor/4.3.1/logging-interceptor-4.3.1-javadoc.jar" = {
788804
url = "https://repo1.maven.org/maven2/com/squareup/okhttp3/logging-interceptor/4.3.1/logging-interceptor-4.3.1-javadoc.jar";
789805
sha256 = "1FDD287ADDB6301F3D16274ADBDF55C7129E2C675DA61EF087A62740EAB49EAF";

src/main/scala/io/iohk/ethereum/App.scala

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package io.iohk.ethereum
22

3+
import io.iohk.ethereum.cli.CliLauncher
34
import io.iohk.ethereum.crypto.EcKeyGen
45
import io.iohk.ethereum.extvm.VmServerApp
56
import io.iohk.ethereum.faucet.Faucet
@@ -17,6 +18,7 @@ object App extends Logger {
1718
val mallet = "mallet"
1819
val faucet = "faucet"
1920
val ecKeyGen = "eckeygen"
21+
val cli = "cli"
2022

2123
args.headOption match {
2224
case None => Mantis.main(args)
@@ -31,11 +33,12 @@ object App extends Logger {
3133
case Some(`mallet`) => Mallet.main(args.tail)
3234
case Some(`faucet`) => Faucet.main(args.tail)
3335
case Some(`ecKeyGen`) => EcKeyGen.main(args.tail)
36+
case Some(`cli`) => CliLauncher.main(args.tail)
3437
case Some(unknown) =>
3538
log.error(
3639
s"Unrecognised launcher option, " +
3740
s"first parameter must be $launchKeytool, $downloadBootstrap, $launchMantis, " +
38-
s"$mallet, $faucet, $vmServer or $ecKeyGen"
41+
s"$mallet, $faucet, $vmServer, $ecKeyGen or $cli"
3942
)
4043
}
4144

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package io.iohk.ethereum.cli
2+
3+
import cats.implicits._
4+
import com.monovore.decline.{Command, Opts}
5+
import io.iohk.ethereum.crypto
6+
import io.iohk.ethereum.crypto._
7+
import io.iohk.ethereum.utils.ByteStringUtils
8+
import java.security.SecureRandom
9+
import org.bouncycastle.util.encoders.Hex
10+
11+
object CliCommands {
12+
13+
val generatePrivateKeyCommand = "generate-private-key"
14+
val deriveAddressCommand = "derive-address"
15+
val generateAllocsCommand = "generate-allocs"
16+
val balanceOption = "balance"
17+
val keyOption = "key"
18+
val addressOption = "address"
19+
20+
private val GeneratePrivateKeyCommand: Command[String] =
21+
Command(name = generatePrivateKeyCommand, header = "Generate private key") {
22+
Opts.unit.map { _ =>
23+
val keyPair = generateKeyPair(new SecureRandom())
24+
val (prvKey, _) = keyPairToByteStrings(keyPair)
25+
ByteStringUtils.hash2string(prvKey)
26+
}
27+
}
28+
29+
private val DeriveAddressFromPrivateKey: Command[String] =
30+
Command(name = deriveAddressCommand, header = "Derive address from private key") {
31+
32+
Opts
33+
.argument[String]("private-key")
34+
.map(Hex.decode)
35+
.map(privKeyToAddress)
36+
}
37+
38+
private val GenerateAllocs: Command[String] =
39+
Command(name = generateAllocsCommand, header = "Generate genesis allocs") {
40+
41+
val keysOpt: Opts[List[String]] =
42+
Opts
43+
.options[String](long = keyOption, help = "Private key")
44+
.map(_.map(key => privKeyToAddress(Hex.decode(key))))
45+
.orEmpty
46+
47+
val addressesOpt: Opts[List[String]] =
48+
Opts.options[String](long = addressOption, help = "Address").orEmpty
49+
50+
val balanceOpt =
51+
Opts.option[BigInt](long = balanceOption, help = "Initial balance for account", metavar = "balance")
52+
53+
(keysOpt, addressesOpt, balanceOpt).mapN { (addressesFromKeys, addresses, balance) =>
54+
allocs(addresses ++ addressesFromKeys, balance)
55+
}
56+
}
57+
58+
private def allocs(addresses: List[String], balance: BigInt): String =
59+
s""""alloc": ${addresses
60+
.map(address => s"""$address: { "balance": $balance }""")
61+
.mkString("{", ", ", "}")}"""
62+
63+
private def privKeyToAddress(privKey: Array[Byte]): String = {
64+
val pubKey = pubKeyFromPrvKey(privKey)
65+
val address = crypto.kec256(pubKey)
66+
Hex.toHexString(address)
67+
}
68+
69+
val api: Command[String] = Command.apply(name = "cli", header = "Mantis CLI") {
70+
Opts.subcommands(GeneratePrivateKeyCommand, DeriveAddressFromPrivateKey, GenerateAllocs)
71+
}
72+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package io.iohk.ethereum.cli
2+
3+
import com.monovore.decline._
4+
5+
//scalastyle:off
6+
object CliLauncher extends App {
7+
8+
CliCommands.api.map(println).parse(PlatformApp.ambientArgs getOrElse args, sys.env) match {
9+
case Left(help) => System.err.println(help)
10+
case Right(_) => ()
11+
}
12+
13+
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
package io.iohk.ethereum.cli
2+
3+
import org.scalatest.EitherValues
4+
import org.scalatest.flatspec.AnyFlatSpec
5+
import org.scalatest.matchers.should.Matchers
6+
7+
class CliCommandsSpec extends AnyFlatSpec with Matchers with EitherValues {
8+
9+
import CliCommands._
10+
import Fixture._
11+
12+
behavior of generatePrivateKeyCommand
13+
it should "generate correct private key" in {
14+
api.parse(Seq(generatePrivateKeyCommand)) shouldBe a[Right[_, _]]
15+
}
16+
17+
behavior of deriveAddressCommand
18+
it should "derive address from private key" in {
19+
api.parse(Seq(deriveAddressCommand, privateKey)).right.value shouldBe address
20+
}
21+
22+
it should "return an error when called without private key" in {
23+
api.parse(Seq(deriveAddressCommand)) shouldBe a[Left[_, _]]
24+
}
25+
26+
behavior of generateAllocsCommand
27+
it should "generate correct alloc using private key" in {
28+
api
29+
.parse(
30+
Seq(
31+
generateAllocsCommand,
32+
argument(keyOption, Some(privateKey)),
33+
argument(balanceOption, Some(requestedBalance))
34+
)
35+
)
36+
.right
37+
.value shouldBe s""""alloc": {$address: { "balance": $requestedBalance }}"""
38+
}
39+
40+
it should "generate more than one alloc" in {
41+
api
42+
.parse(
43+
Seq(
44+
generateAllocsCommand,
45+
argument(keyOption, Some(privateKey)),
46+
argument(keyOption, Some(privateKey2)),
47+
argument(balanceOption, Some(requestedBalance))
48+
)
49+
)
50+
.right
51+
.value shouldBe s""""alloc": {$address: { "balance": $requestedBalance }, $address2: { "balance": $requestedBalance }}"""
52+
}
53+
54+
it should "generate allocs using addresses" in {
55+
api
56+
.parse(
57+
Seq(
58+
generateAllocsCommand,
59+
argument(addressOption, Some(address)),
60+
argument(addressOption, Some(address2)),
61+
argument(balanceOption, Some(requestedBalance))
62+
)
63+
)
64+
.right
65+
.value shouldBe s""""alloc": {$address: { "balance": $requestedBalance }, $address2: { "balance": $requestedBalance }}"""
66+
}
67+
68+
it should "generate allocs using both keys and addresses" in {
69+
api
70+
.parse(
71+
Seq(
72+
generateAllocsCommand,
73+
argument(addressOption, Some(address)),
74+
argument(keyOption, Some(privateKey2)),
75+
argument(addressOption, Some(address3)),
76+
argument(balanceOption, Some(requestedBalance))
77+
)
78+
)
79+
.right
80+
.value shouldBe s""""alloc": {$address: { "balance": $requestedBalance }, $address3: { "balance": $requestedBalance }, $address2: { "balance": $requestedBalance }}"""
81+
}
82+
83+
}
84+
85+
object Fixture {
86+
87+
def argument(name: String, value: Option[Any] = None): String = s"--$name${value.fold("")(v => s"=${v.toString}")}"
88+
89+
val privateKey = "00b11c32957057651d56cd83085ef3b259319057e0e887bd0fdaee657e6f75d0"
90+
val privateKey2 = "00b11c32957057651d56cd83085ef3b259319057e0e887bd0fdaee657e6f75d1"
91+
val privateKey3 = "00b11c32957057651d56cd83085ef3b259319057e0e887bd0fdaee657e6f75d2"
92+
93+
val address = "8b196738d90cf3d9fc299e0ec28e15ebdcbb0bdcb281d9d5084182c9c66d5d12"
94+
val address2 = "add8c627e14480b36b30811758240d8acb282aae883043990d8a2d7e2e75cf3b"
95+
val address3 = "1e9cd60cf3b2c902e60f809e604542f9a9fb55d3e8004ff122f662f88eb32b4a"
96+
97+
val requestedBalance = 42
98+
99+
}

0 commit comments

Comments
 (0)